Skip to content

Architecture

SAE Books is built around a clean boundary: the API is the product, and everything else — the web UI, document vault, accounting-package sync, ATO lodgement relay, licensing server — is a service orbiting it.

This is not a cosmetic distinction. The API is the AGPLv3 licensed artifact. Every service connects to it over HTTP, the same way a third-party integration would. No service reaches into the API's Postgres schema directly.

The orbit

graph TB subgraph "On-prem (customer's server)" API["saebooks-api
FastAPI · SQLAlchemy · Postgres
port 8042 (REST) · 50051 (gRPC)"] WEB["saebooks-web
Jinja2 · HTMX
port 8080"] PG[("Postgres 16
+ RLS")] API --- PG WEB -->|"REST calls"| API end subgraph "SAE Engineering services" LICENSE["license.saebooks.com.au
Ed25519 JWT · Stripe"] LODGE["lodge.saebooks.com.au
ATO SBR relay · STP · BAS"] FEEDS["feeds.saebooks.com.au
CDR bank-feed relay
(SISS Data Services)"] MARKETING["saebooks.com.au
marketing · Stripe Checkout"] end subgraph "Third-party services (customer-operated)" SYNC["saebooks-sync
Xero / MYOB / QBO
bidirectional adapter"] VAULT["saebooks-vault
invoice attachment store"] DISCOURSE["discourse.saebooks.com.au
forum · DiscourseConnect SSO"] end API -->|"licence check"| LICENSE API -->|"lodge STP/BAS"| LODGE API -->|"bank feed pull"| FEEDS SYNC -->|"REST"| API VAULT -->|"REST"| API WEB -->|"DiscourseConnect"| DISCOURSE

On-prem components

saebooks-api — the ledger. FastAPI with async SQLAlchemy 2.x on Postgres. Every accounting operation goes through the REST API. No side-channel writes. Runs on port 8042.

saebooks-web — the browser UI. FastAPI + Jinja2 + HTMX. No JavaScript build step (Tailwind CSS is pre-compiled at image build time). Holds a signed session cookie (itsdangerous) that stores the API bearer token. Proxies all data operations through saebooks-api. Runs on port 8080 in production; 8043 in local dev.

Postgres — system of record. Row-level security on every domain table, keyed on tenant_id. The current tenant is bound per-request via SET LOCAL app.current_tenant = '...'. Cross-tenant access is an explicit, audited code path — not a query-parameter.

SAE Engineering relays

These run on SAE Engineering's infrastructure. Customer installs call out to them; SAE Engineering holds zero customer accounting data.

license.saebooks.com.au — Ed25519 JWT licence minting. Stripe Checkout webhook → issue trial or paid licence → email to customer. The JWT the customer pastes into their .env is verified locally by the API binary (no network call on every request — only a once-daily refresh).

lodge.saebooks.com.au — ATO SBR relay. Signs and forwards STP/BAS/TPAR/SuperStream envelopes through SAE Engineering's Machine Credential. Customer never touches the credential. SBR lodgement is a Pro+ feature; the lodge-server verifies the licence JWT before forwarding.

feeds.saebooks.com.au — CDR bank-feed relay. Proxies through SAE Engineering's SISS Data Services (ACSISS) agreement. Customers bring their bank consent; SAE holds the data-recipient credential. Enterprise feature.

saebooks.com.au — marketing site and Stripe Checkout. Not part of the self-hosted stack.

Third-party services

saebooks-sync — bidirectional accounting-package sync (Xero, MYOB, QBO). Not yet shipped. See Sync overview.

saebooks-vault — invoice attachment and document store. Postgres metadata + blob store, separate service. Planned, not yet shipped.

Discourse — community forum at discourse.saebooks.com.au. Discourse is the DiscourseConnect identity provider and SAE Books is the consumer: the web process's /auth/discourse/login redirects to Discourse's /session/sso_provider, Discourse authenticates the user, and SAE Books consumes the signed identity payload on return. See DiscourseConnect.

Request path (browser to data)

Browser
  → Caddy / reverse proxy (TLS termination)
  → saebooks-web :8080  (session auth, Jinja render, CSRF)
  → saebooks-api :8042  (JWT auth, business logic)
  → Postgres :5432      (RLS-enforced query)

State-changing requests carry a CSRF token (double-submit cookie pattern). Origin/Referer enforcement runs before CSRF.

Multi-tenancy model

Every domain table carries tenant_id UUID NOT NULL. Postgres RLS policies enforce per-tenant visibility. Each install is single-tenant by default; Enterprise licences enable multi-tenant operator tooling.

The discipline holds even on single-tenant installs. No global queries. Every insert asserts tenant_id. This is what makes a future cloud-hosted pivot a deployment change rather than a rewrite.

See Tenancy & RLS for the implementation detail.

Design philosophy

  • The API is the product surface. The OpenAPI spec at /openapi.json is the contract. If it's not in the spec, it doesn't exist.
  • Stateless app servers. All state lives in Postgres. The app container is disposable — docker compose up -d --build and the new container picks up exactly where the old one left off.
  • External services called by config URL. LICENSE_SERVER_URL, LODGE_SERVER_URL, FEEDS_SERVER_URL default to the SAE Engineering endpoints but can be overridden. An air-gapped install can point at its own relay.
  • No vendor lock-in. Your data is in Postgres. Dump it any time. Import it into anything that can read Postgres. The sync service connects outbound to Xero/MYOB/QBO — not the other way around.

Built for the next 20 years, not the last 20.