Skip to content

System Overview

RBO V2 is a small server-rendered web application for tennis-racket stringers — multi-tenant from day one, sub-thousand stringers, sub-100k orders total over a multi-year horizon. It replaces the V1 XLSX + static-page-and-PDF read-only site and becomes the system of record.

Confidence: High.

30-second mental model

A single Python web application talks to a Postgres database (one db per env, on Atlas's shared cluster), generates A4 PDF receipts via WeasyPrint, sends transactional email via Resend's SMTP relay (per keystone ADR-0005), and authenticates stringers through self-hosted gotrue (Atlas's auth runtime). Every read and write is automatically scoped by stringer_id at the ORM-session layer; deliberate cross-tenant access is mediated through the three-grant sharing model (Rules #1, #2, #3 — order_shares + person_stringer_share per ADR-0004).

Stack

Layer Choice Why
Backend framework FastAPI (Python 3.12+) Matches V1's Python smell; fits Atlas's container model; type-hint-driven request validation removes a class of bugs; async stays out of the way at our load.
ORM SQLAlchemy 2.0 Mature, supports the session-event hook needed to enforce tenancy (see auth-and-tenancy); composable with Alembic for migrations.
Migrations Alembic The default for SQLAlchemy. Migrations live in the app repo and run as a CI step.
Database Postgres 16 Provided by Atlas's platform (db-per-app, db-per-env, PgBouncer in front). Native partial unique indexes + enum types are load-bearing for the schema (see data-model).
Templates Jinja2 (server-rendered HTML) No SPA — domain is form-and-list CRUD. Server-rendered pages with light progressive enhancement match Atlas's stack (Python container) and Stefan's NFR Topic 5 answers (modern evergreen browsers, no PWA in V2).
Frontend styling Tailwind CSS Mobile-first responsive without a JS build step beyond tailwindcss watch. Latest evergreen browser target lets us drop legacy polyfills.
Frontend interactivity HTMX + sprinkled vanilla fetch Covers "search client → copy last order" prefill and the moderation-queue actions without a SPA framework.
PDF rendering WeasyPrint (HTML+CSS → PDF) A4-native; pixel-control we need for the top-left-quarter total constraint (see receipts); designers iterate the receipt as a normal HTML page.
Email Resend SMTP (smtp.resend.com:587) via aiosmtplib Per keystone ADR-0005. DKIM via resend._domainkey.wagen.io; SPF/DMARC managed by Atlas at the platform level. Free-tier caps (100/day, 3000/mo) are well above our V2 volume. See integrations.
Auth runtime gotrue (Atlas's self-hosted Supabase Auth) RBO consumes JWTs; it does not run its own auth. Magic-link slot for V3 is already in gotrue's vocabulary. See auth-and-tenancy.
i18n Python gettext via Babel + Jinja2 {% trans %} EN + DE for UI; per-language receipt templates. See i18n.

Component layout

                                                ┌──────────────────────┐
                                                │   Stefan / stringer  │
                                                │   browser (mobile +  │
                                                │   desktop, concur.)  │
                                                └──────────┬───────────┘
                                                           │ HTTPS
┌─────────────────────────── keystone platform (Atlas) ────────────────────────┐
│                                                                              │
│   ┌────────────┐    ┌──────────────────────────────────────────────────┐    │
│   │  Caddy     │───▶│  RBO container (FastAPI + Jinja + WeasyPrint)    │    │
│   │  (TLS,     │    │   • route handlers                                │    │
│   │   HSTS,    │    │   • SQLAlchemy session w/ tenant filter          │    │
│   │   wildcard │    │   • PDF render path                               │    │
│   │   cert)    │    │   • SMTP wrapper                                  │    │
│   └────────────┘    └──┬─────────────────┬──────────────┬───────────────┘    │
│                        │                 │              │                    │
│                        ▼                 ▼              ▼                    │
│                  ┌──────────┐      ┌──────────┐   ┌────────────┐             │
│                  │ PgBouncer│      │ gotrue   │   │ Resend     │             │
│                  │  → PG 16 │      │ (auth)   │   │ SMTP relay │             │
│                  │ rbo_prod │      │ rbo.auth │   │            │             │
│                  │ rbo_test │      │ schema   │   │            │             │
│                  └──────────┘      └──────────┘   └────────────┘             │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

Everything inside the dashed box is owned by Atlas's platform proposal. RBO's footprint is the single FastAPI container plus its Postgres database and auth schema.

How RBO sits on Atlas's platform

  • Hosting: one Compose service in /srv/apps/rbo/compose.yml on the Hetzner CCX13 VPS. Sized at 256–512 MB RAM (Atlas §1).
  • Ingress: rbo.wagen.io (prod) / rbo-test.wagen.io (test) routed by Caddy via the *.wagen.io wildcard cert. A future custom domain (e.g. racketbook.com) drops in via Atlas's Caddy snippet recipe; RBO needs no code change.
  • Database: rbo_prod and rbo_test databases on the shared cluster, accessed through PgBouncer at pgbouncer:6432. Schema is owned by RBO's Alembic migrations.
  • Auth: RBO registers as a gotrue tenant per env (auth schema inside rbo_prod / rbo_test). RBO never sees passwords; gotrue mints JWTs, RBO validates them.
  • Backups: automatic via Atlas's nightly pg_dump — RBO does nothing extra. The product-level per-stringer XLSX + JSON exports (integrations) are an RBO feature, not a backup mechanism.
  • CI/CD: RBO repo includes the keystone CI template; push-to-main deploys test, manual job promotes to prod.
  • Logs/metrics: RBO emits JSON-to-stdout per Atlas's convention. Caddy and Uptime Kuma do the rest.

What RBO is NOT responsible for

  • TLS termination (Caddy does it).
  • Postgres ops, backup, restore (Atlas's runbooks).
  • Auth primitives — password hashing, magic-link token mint/verify, session/cookie internals (gotrue does it).
  • Host-level security, firewall, OS patches (Atlas).
  • Cross-app concerns (CSD, ALJ, future apps each have their own stack on the same platform).

This boundary is intentional: RBO is application code on top of a shared platform; it does not duplicate platform concerns.

Deployment topology summary

  • Single container, single process model. No background workers in V2 — the only outbound paths (receipt email at Strung-date, admin moderation notification) are synchronous from the request that triggers them. If V3 notification fan-out grows, a small worker (RQ/arq) is the natural addition; the current shape doesn't need it.
  • Stateless app container. No on-disk state in the container. Generated PDFs are streamed to the response (or to SMTP attachment); they are not persisted between requests. Re-generation from order data is always the source of truth — see receipts.
  • Two envs, two dbs, one image-per-env build. Matches the keystone CI template's NEXT_PUBLIC-style env-baking, even though RBO has no NEXT_PUBLIC_* exposure (server-rendered). Build-per-env is kept for symmetry with CSD/ALJ; runtime env-vars carry the per-env config.

What's deferred to later docs