Skip to Content
Working with coding agentsWhat a coding agent is handed

What a coding agent is handed

Applies to The Fabrica v0.1.2. Documentation source: 1ee1bc9cb661, including post-release documentation corrections.

What this repo is

A production SaaS built on The Fabrica — a Python/FastAPI + Next.js boilerplate. Backend under backend/, frontend under frontend/. If you were forked from the factory, the code you see is the starting point; the buyer extends it for their own product.

Stack: FastAPI + async SQLAlchemy 2.0 + Postgres + Celery + Redis (backend); Next.js 16 + React 19 + Tailwind + next-intl (frontend); Clerk (auth), Paddle (payments, Merchant of Record), Resend (email), Anthropic/OpenAI (LLM).

Verification (run before declaring any change done)

Backend:

uv run ruff check backend/src backend/tests backend/alembic uv run mypy --strict --ignore-missing-imports backend/src uv run pytest backend/tests/ -m "not integration" # fast; no DB needed uv run pytest backend/tests/ # full; needs Postgres+Redis

Frontend:

cd frontend npm run lint # eslint --max-warnings 0 npm run type-check # tsc --noEmit npm test # vitest run

After a model change, and before the tests above:

uv run alembic revision --autogenerate -m "..." uv run alembic upgrade head

Local services: docker compose up -d postgres redis. Run npm run build too when you touch a page — a server/client boundary error surfaces only there, never in type-check.

CI runs all of these. mypy --strict and eslint --max-warnings 0 are non-negotiable — the bar is zero errors, zero warnings.

Generating the code is not finishing the task

A diff that exists is not a change that works. Before reporting anything done:

  1. Run the checks above that the change could plausibly break — not only the ones you expect to pass.
  2. Run the tests covering the changed behaviour, and add one if none existed. A failing test is information: never delete, skip or xfail one to reach green.
  3. Observe the actual system state, not just the exit code — the row in the database, the HTTP response, the rendered page, the queued job, the audit entry.
  4. Say plainly what you did not verify — an unverified claim of completion costs more than an honest gap.

Invariants — do NOT change these without a very good reason

These are load-bearing. Changing them cascades:

  • backend/src/db/base.py:NAMING_CONVENTION — Alembic migration determinism depends on it. Touch it and downgrades break across machines.
  • backend/src/services/repositories_async/base.py — the repository pattern. AsyncBaseRepository deliberately exposes no ownership-aware read; each user-owned resource declares get_for_user / list_for_user on its own repository (the scaffold’s repository.py.tmpl generates them), so that resource’s ownership filter lives in ONE place. A router calling repo.get(...) or session.get(Model, id) has bypassed it — an IDOR red flag.
  • backend/src/api/deps.pyCurrentUserDep / AdminDep / DBDep. The auth contract. AdminDep gates on the email allowlist (project_config.admin_emails), re-checked per request.
  • backend/src/startup_guard.py — boot-time invariant checks. Refuses to start with sentinel secrets / TBD config. Don’t weaken to “just deploy”.
  • Celery serializer = JSON only (workers/celery_app.py). Pickle is an RCE vector. Never switch it.
  • AuditLog on every privileged action via services/audit.py:log_action_async. It’s the GDPR Art. 30 surface.

Money, jobs and account lifecycle carry four more. These are the ones an agent breaks by writing the obvious thing:

The _domain/ convention (buyer code vs factory code)

Buyer-specific code lives in _domain/ subdirectories; factory code lives one level up. Upstream owns the README.md and .gitkeep inside those directories and may revise them, but it ships no other file there — so a file you create has no upstream counterpart to be overwritten by. That shrinks the surface an update can collide with; it does not make updates conflict-free. Every reserved directory:

backend/src/models/_domain/ backend/src/api/routers/_domain/ backend/src/services/_domain/ backend/src/workers/tasks/_domain/ backend/src/prompts/_domain/ backend/src/scrapers/_domain/ backend/data/seed/_domain/ frontend/src/components/_domain/ frontend/src/app/[locale]/(dashboard)/(_domain)/

The frontend route one is a route group — parenthesised. A bare _domain/ there is a Next.js private folder and never routes, so a page put in one is silently unreachable.

Protection comes from where the file lives, not from the merge: your files at reserved paths have nothing to reconcile against, while edits to factory files — including the _domain/ READMEs, if you change them — merge like any other change. See docs/recipes/buyer-extension-model.md.

Where things live (quick map)

backend/src/ api/routers/ one file per resource (account, billing, admin, ...) api/deps.py CurrentUserDep / AdminDep / DBDep services/ email, paddle, clerk_webhook, audit, llm/, unsubscribe models/ User, Subscription, CreditTransaction, AuditLog, ... workers/ celery_app, credit_ledger, reconciler, beat tasks middleware/ security_headers, llm_rate_limit config/ settings, project, tiers, brand startup_guard.py boot-time invariant checks frontend/src/ app/[locale]/ locale-prefixed routes (marketing + (dashboard)) components/ ui/, providers/, banners hooks/ use-api, use-clerk, use-utm-capture docs/ recipes/ + architecture/ + compliance/ + security/ bin/templates/ resource-scaffold (CRUD templates; no generator command)

Full architecture: docs/architecture/overview.md.

Style

Match the surrounding code: comment density, naming, idioms. The codebase favours explicit over clever, boring over novel. Every non-obvious decision gets a short comment explaining why, not what.


Provenance. Documentation source: 1ee1bc9cb6619c19f57766731e7884b37f515dc9 (main).

Extracted from CLAUDE.md.

That commit is later than the release these pages describe: it carries documentation corrections made after thefabrica-v0.1.2 went out. Nothing that changes how the product behaves landed between the two, which is what makes naming that version honest.

Last updated on