Surge · internal tools platform · 2025—26
Turning one-off scripts into self-serve tools behind a single login
A suite of internal tools for marketing and sales — performance audits, structured-data generation, bulk content and pricing — on one FastAPI backend with shared authentication. I wrote most of it: the Web Vitals platform, the schema generator, the pricing calculators and the access model that ties the apps together.
The problem
Every data request was a ticket, and every ticket became a script on someone's laptop
Web Vitals reports, structured data for blog posts, pricing comparisons for sales calls — all of it could only be produced by an engineer. Each request was solved once with a script that nobody else could run, and then solved again next month.
The fix wasn’t another script. It was turning the recurring ones into small applications the team could use on their own, behind one login, on one backend that’s cheap to extend.
Architecture
Many small apps, one backend, one way in
Each tool is its own app in the UI — its own routes and layout — but they share a FastAPI backend layered as routes, services and models, a single authentication system, and one database. Long jobs run in the background and stream progress back; recurring ones run on a schedule.
Web Vitals at scale
Performance audits anyone can run, batch or schedule
The Web Vitals app replaced ad-hoc PageSpeed checks with a proper workflow: test a page, test a batch, or put a set of pages on a schedule and watch the trend.
- 1A test is created as a record with a lifecycle — pending, running, completed or failed — and handed to a background task, so the request returns immediately.
- 2The service calls the performance API for mobile and desktop, keeping at least two seconds between calls and backing off on rate limits using the server’s own Retry-After, with a single retry on server errors.
- 3Lab metrics (LCP, INP, CLS, FCP, TTFB, TBT, score), real-user field data, opportunities and diagnostics are stored per run, so a page’s history is a query.
- 4Batches retry failed pages and compute aggregates; schedules hold a cron expression, and a scheduler wakes due runs and computes the next one.
Schema generation
Structured data for hundreds of posts, generated and validated
The schema app generates HowTo structured data for blog posts. Input is a URL or a batch of URLs from a shared spreadsheet; pages are fetched five at a time, an LLM drafts the schema, and the result is validated, enriched (for example, total time estimated from the steps) and saved. Progress streams to the browser over server-sent events, so a long batch is something you watch rather than wait on.
One login, many apps
Adding an app shouldn't mean editing every user
Access is defined alongside each account and returned when the app loads; the app selector shows only the tools a person is provisioned for, and routes they can’t use redirect home. A wildcard grants everything, so shipping a new app doesn’t require touching every existing user — and accounts without an explicit list keep working as before.
The pricing calculators live entirely in the browser: they model usage-based and pipeline-based pricing side by side, handle the edge cases between tiers, and export a clean PDF for a sales conversation.
Practices
What keeps it easy to extend
Layered backend
Routes stay thin; services own the logic and external calls; models and Pydantic schemas define the contracts on both sides of the wire.Typed contracts end to end
Pydantic v2 on the server, TypeScript with zod and react-query on the client.Same code, two databases
Async SQLAlchemy runs on SQLite locally and Postgres in production, switched by connection string alone.Long work off the request path
Background tasks, scheduled runs and streamed progress mean nothing important happens inside a single HTTP request.
The next step is the one internal tools usually skip: automated tests and CI checks around the shared auth and the scheduler, so the platform stays as safe to extend as it is easy.
Built with
- Python
- FastAPI
- SQLAlchemy
- asyncpg
- PostgreSQL
- React
- APScheduler