Available for hire

Jeffrey Lowe

Full-Stack & Python Developer

I designed, shipped, and maintain a production Django web app end to end, solo. Plus a companion Python bot service and a React Native mobile app, both in active development. Self-taught and looking for my first developer role.

Jeffrey Lowe
Case Study / 01

PlatPursuit

PlayStation trophy tracking, end to end. Production Django web app with active users, payment flows, and ongoing maintenance.

Solo author 2024 – present Production Active maintenance
PlatPursuit dashboard showing recent platinums and personal stats modules
30+
Django models
49
system docs
41
dashboard modules
Live
in production

What I built

  • Schema and ORM

    30+ Django model schema covering games, trophies, badges, dashboards, and payments. Includes a Concept.absorb() migration method that safely merges 15+ related models during sync reassignments, preserving relational integrity when PSN reassigns concepts upstream.

  • Third-party API integration

    Six-strategy IGDB matching pipeline with retry-on-401, no-match persistence, and platform-overlap enforced as a hard requirement. Handles cover art, metadata, and Tier-2 stats end to end across thousands of games.

  • Frontend and responsive design

    Customizable 41-module dashboard with a documented three-layout system (375 / 768 / 1024 px). Mobile-first, dark-theme-aware. The design system is the canonical reference for every page in the app.

  • Database performance

    Diagnosed and fixed a production OOM caused by select_related eagerly fetching a ~30 KB JSON column across every cover-art render. Repair was targeted .defer() chains paired with select_related, with explicit .only() opt-in only where the JSON was actually needed. Also ships Subquery/OuterRef workarounds for Django's .update() SET-clause limits.

  • Process maturity

    Every system has a doc with a mandatory Gotchas and Pitfalls section. When behavior changes, the relevant doc updates in the same branch. Three-phase quality workflow: plan with a reuse check, build with inline audits, polish before merge.

The signature story

Designing TokenKeeper: a custom worker pool for Sony's PSN API

Sony doesn't expose a public OAuth flow for third parties. PlatPursuit authenticates with PSN using NPSSO cookies, which mint short-lived access tokens whose state needs to flow cleanly through every checkpoint of a sync pipeline: queue dispatcher, per-token worker, rate limiter, response parser, DB writer. Celery is closed off in that respect; every task hand-off goes through pickle byte-stream serialization, which forces lossy conversion and implicit assumptions at each step. TokenKeeper keeps that state in-process and JSON-shaped, so each piece of the pipeline reads exactly what the previous one wrote, no decoding ambiguity.

So I built it: one long-lived Python process, in-memory token pool, no broker.

TokenKeeper runs three worker threads per token group, pulling jobs from a five-tier Redis priority queue (orchestrator, high, medium, low, bulk). The bulk tier exists specifically so whale accounts with 5,000+ games can't starve normal users. Each token is wrapped in a TokenInstance dataclass tracking busy state (atomic Redis-based locking), expiry, an LRU cache of user lookups, and outbound IP for proxy routing. A health loop runs every 60 seconds: it refreshes any token within 5 minutes of expiring, publishes stats over Redis pub/sub for the dashboard, and runs a circuit breaker that trips on five PSN 5xx errors in a 60-second window. When PSN goes down, sync timestamps get backdated and writes pause cleanly until it recovers.

The hardest bug in this system wasn't conceptual; it was psnawp's default rate limiter, which uses a SQLite-backed bucket and produced "database is locked" errors under concurrent load. The fix was a small custom PSN client subclass that swaps in an in-memory bucket. A PSNManager facade hides all of this from the rest of the app: views, cron handlers, and API endpoints call assign_job() and never see the worker. One Python process owns the full PSN surface: auth, rate limiting, retries, priority queueing, and observability. No broker, no result backend, no separate worker pool to scale. The takeaway: when the obvious choice imposes a heavier operational burden than the workload justifies, building the right thing custom is often cheaper to operate and simpler to reason about. Especially when stateful auth sits at the core of the workload.

Stack

PythonDjangoPostgreSQLRedisTailwinddaisyUIPSN APIIGDB API
Case Study / 02

PlatBot

Companion Discord bot for the PlatPursuit ecosystem. discord.py and FastAPI under one asyncio event loop, with async workers for rate-limit-aware role syncing.

Solo author 2025 – present Active development
PlatBot responses to /summary and /trophystats commands in Discord, showing PSN profile data and community trophy totals
14
slash commands
2
webhooks
12
API endpoints
Active
development

What I built

  • Multi-framework architecture

    discord.py and FastAPI run in the same asyncio event loop, coordinated via asyncio.gather. FastAPI is embedded as an inbound webhook receiver, not a separate microservice; PlatPursuit pushes role updates over HTTP, the bot reflects them to Discord. One process, dual runtime, no broker, no IPC.

  • API contract design

    Pydantic schemas at every boundary. The RoleRequest model validates inbound webhooks; outbound calls to PlatPursuit's REST surface go through a persistent aiohttp.ClientSession with token auth headers attached, reused across all 14 commands and the worker pool. Twelve PlatPursuit endpoints consumed.

  • Async patterns

    asyncio.Queue with background workers for rate-limit-aware role operations. Deferred slash commands (interaction.response.defer) to respect Discord's 3-second interaction timeout. Async View button callbacks for multi-step flows. Signal handlers (SIGTERM, SIGINT) cancel tasks in order and drain queues before close, so in-flight operations don't drop on deploy.

  • Discord rate-limit handling

    Discord's role API caps at a small burst before returning 429. Workers detect the 429, read the Retry-After header from the response (not a guess), sleep the exact interval requested, and retry up to five times before giving up. The webhook never blocks on this; it returns 202 Accepted and moves on. Pragmatic resilience, not a sophisticated library.

  • Feature breadth

    14 slash commands covering account linking, trophy summaries, paginated trophy cases, role syncing, and community-wide stats with timezone-aware aggregation. Member event handlers for join welcomes and auto-unlinking on leave. Mod tooling for forced refreshes and badge audits. Each command in its own discord.py Cog file.

The signature story

Embedding FastAPI inside discord.py's event loop

PlatPursuit needs to push role changes to Discord: badge unlocks, milestone achievements, premium tier upgrades. Discord's role API is aggressively rate-limited (small burst, then 429), so a synchronous webhook would either time out or fail under load. The cleanest version of this system is one HTTP boundary: PlatPursuit calls PlatBot, PlatBot reflects the change to Discord. The catch is that the bot is already speaking to Discord over a long-lived gateway connection, and that connection lives on an asyncio event loop. Adding a second process would duplicate the auth surface and force two deployments. Adding a thread for HTTP would fight the loop.

The fix is one event loop. FastAPI runs as an asyncio.create_task alongside bot.start(TOKEN), both gathered via asyncio.gather. There's no separate microservice, no broker, no IPC: PlatPursuit POSTs to /assign-role, FastAPI validates the payload against a Pydantic schema, drops the work onto an asyncio.Queue, and returns 202 Accepted immediately. Two background workers (assign + remove) drain the queues continuously. When Discord returns a 429, the worker reads the Retry-After header (not a guess), sleeps the requested interval, and retries up to five times before giving up. Outbound calls back to PlatPursuit's API reuse a persistent aiohttp.ClientSession with token auth attached, so every cog and worker shares a connection pool.

Single process, dual runtime. The webhook endpoint never blocks on Discord; the bot never blocks on HTTP; neither blocks on PlatPursuit's REST surface. Signal handlers (SIGTERM, SIGINT) cancel tasks in order and drain the queues before close, so role operations in flight don't get dropped on deploy. The takeaway: when two frameworks are both async and share a runtime model, embedding one inside the other is often cheaper than separating them. The contract is a Pydantic schema and a queue, not a service mesh.

Stack

Pythondiscord.pyFastAPIaiohttpPydanticDiscord API
Working Principles

How I work

Five things that show up in everything I ship.

Plan → Build → Polish

Every feature goes through three phases. Planning surfaces existing code that already solves the problem (the rule of three: no abstraction until three concrete uses exist). Build runs background audits on each completed chunk to catch quality drift while context is fresh. Polish reviews every changed file against the quality bar before merge. Each phase catches different categories of issue at the cheapest possible time.

Docs ship in the same branch as the code

PlatPursuit has 49 system docs, each with a mandatory Gotchas and Pitfalls section. When a system's behavior changes, its documentation updates in the same branch. Stale docs are worse than no docs. The discipline pays off three ways: future-me onboards faster, present-me explains decisions clearly, and the writing surfaces design problems before they ship.

AI tools are part of my workflow

Claude Code accelerates the build phase. Architecture, system design, and every decision about what to ship and what to defer are mine. The three-phase workflow above exists in part because the parts AI is good at sit between two phases that have to stay human: planning before, audit after. I review every line that ships, and I don't merge code I can't defend in an interview.

Search before you build

Before finalizing any plan, I search the codebase for utilities, helpers, and patterns that already cover the proposed work. If existing code covers the need, I reuse it. If it almost covers it, I extend it. New abstractions only get created when nothing suitable exists. Three similar lines is fine; speculative generalization for a future requirement that may never come is technical debt with extra steps.

Build what's needed, not what might be

Scope follows the task, not what the task might become. A bug fix doesn't need surrounding cleanups; a one-shot operation doesn't need a helper; new abstractions wait until three concrete uses exist. Unused code gets deleted, not deprecated or wrapped behind a flag. Validation lives at system boundaries (user input, external APIs); internal code is trusted to do what it says.

Get in touch

Let's talk

I'm actively looking for full-stack, backend, or Python developer roles. Email is the fastest way to reach me, and the resumes below are tailored to each track.