← Projects

active

Hawzu

Execution-first test management for QA teams.

Role
Founder & sole engineer
Period
Apr 2025 — Present
Stack
PythonReactCloud infrastructureMongoDBFastAPIRedisTypeScriptAstro
  • 4 Web properties
  • 2 API surfaces
  • 1 Engineer

Most test management tools are, underneath, a database of test cases with reporting bolted on. They answer “what did we test?” well. They answer the question every release meeting actually asks — are we ready to ship? — badly, or not at all.

Hawzu is built the other way round. Execution is the centre of the model, and everything else exists to turn execution into a defensible shipping decision.

The shape of it

Hawzu system architecture Five static sites served from Cloud Storage buckets behind one global HTTPS load balancer with CDN. The browser app calls a FastAPI service on Cloud Run, where a single middleware pipeline — CORS outermost, then authentication, authorization and auditing — fronts both API surfaces: one for the application, authenticated by user session, and one for automation, authenticated by service account. Every Cloud Run replica also runs a background job scheduler, coordinated by an advisory lock held in MongoDB. State lives in MongoDB, with Redis as a losable cache and Qdrant for vector search. EDGE — production-lb + Cloud CDN www · docs · blog srinathreddy.dev app — React SPA 5 GCS buckets, one url-map XHR CLOUD RUN — backend-service (autoscaled) Middleware pipeline · CORS outermost Authentication Authorization Auditing /ui/v1 — application API user session /api/v1 — automation API service account Job scheduler — in every replica 3 Mongo-backed queues · periodic sweeps serialised by a leased advisory lock in Mongo Python 3.11 · uvicorn · one image STATE MongoDB all state · queues locks · logs Redis cache only losable Qdrant vectors optional
Requests enter through the load balancer; only the app calls the API. Inside Cloud Run one middleware chain serves both API surfaces — the public one differs by the kind of token, not the transport. Every replica also drains the job queue, which is why the advisory lock (highlighted) is load-bearing rather than an optimisation. Solid arrows are required paths; dashed ones the system runs without.

What I built

The whole thing, end to end. There is no team.

Backend — FastAPI over MongoDB, with Redis as a cache. Two API surfaces share one middleware chain: /ui/v1 for the app, and a public /api/v1 for automation. Automation authenticates with scoped service accounts rather than user sessions. They behave similarly, but their lifetime belongs to the automation, not to a person’s browser session.

One small decision illustrates the philosophy: the middleware is ordered so that failures stay truthful. Authentication and authorization can both reject a request, and when they do the caller needs to see that — not a transport-level error standing in front of it. An outage you can’t name is worse than the outage.

Frontend — a React + Vite SPA on Mantine, plus three static Astro sites (marketing, docs, blog) that deploy independently to CDN-backed buckets.

Infrastructure — one deployable API. Everything in front of it is static and ships on its own schedule, behind a single shared edge. A docs fix or a blog post goes out without redeploying the product, and nothing on that side can take it down.

Decisions worth writing down

Execution before storage

The data model treats a test execution as the primary record and the test case as the thing being executed. That inversion is why release readiness is computable at all: coverage, defects, and requirement traceability all hang off executions, so the evidence is already connected when someone asks whether to ship.

What it cost: almost every query is scoped through an execution, so the handful of genuinely test-case-centric views — “where is this case used?” — are the awkward ones. That was the right trade only because the shipping question is asked far more often than the inventory question.

A release snapshot, not a moving target

Release analytics used to shift under you: link a defect after sign-off and last month’s numbers changed.

The obvious fix — lock defects once a release completes — was wrong. Defects should keep moving; a bug found in production after sign-off is still that release’s bug, and its status genuinely changes. What must not move is the evidence the decision was made on. So the release, its executions and their test cases are held read-only by the editability guards while frozen, and the one remaining source of drift — the defect-derived analytics — is snapshotted at the moment the release completes, with the snapshot’s as_of becoming the frozen clock everything downstream reads from.

What it cost: a second read path. Every consumer of release analysis has to know whether it’s looking at live data or a snapshot, and the UI has to say which it’s showing. That’s real complexity bought to keep one property: sign-off means something only if the evidence behind it stops moving.

Jobs in Mongo, not a broker

The background queue started on Redis/RQ and moved into MongoDB — the database already being operated — drained by a bounded thread pool inside the API process itself. Redis stayed, as a cache only.

The interesting part isn’t the store, it’s what Cloud Run does to the design. Every replica has the scheduler enabled, and Cloud Run autoscales, so every replica tries to drain the same queue. Rather than run a separate worker service, correctness comes from a Mongo-backed advisory lock with a lease and heartbeat: whoever claims a job runs it, a live holder keeps its lease fresh, and a holder that dies has its lock taken over once the lease goes stale. The same mechanism guards the periodic sweeps, which would otherwise run once per replica.

What it cost: the lock is now load-bearing infrastructure — a correctness bug there is a double-executed job, not a slow one. In exchange there’s no worker tier to deploy, scale or monitor separately.

A cache you can lose

Redis holds sessions, user metadata and permission lookups. When it isn’t there — at startup or halfway through a request — lookups fail fast and fall through to MongoDB rather than waiting on a connection that isn’t coming back. Slower, still correct.

The rule this encodes: if losing the cache takes the product down, it was never a cache. It was an undocumented database.

Each of these left behind something more general than itself. The versions I’d carry to the next system are written down as principles.

Where it is now

In early access, bootstrapped, with every feature free while the product finds its shape.

Currently in development, not yet shipped: CI/CD automation — triggering and importing builds from Jenkins, GitHub Actions and GitLab CI. The design problem that made it interesting is what to do with results that match no known test case. Discarding them loses data; force-matching them invents coverage nobody ran. They get their own collection instead, deliberately outside the one the analytics read from, so an unreconciled pile can never quietly become a number on a readiness scorecard.