Skip to content

Developer FAQ

Common questions from developers integrating with or extending SAE Books.

Licensing and forking

Can I fork SAE Books?

Yes. The source is published under AGPLv3 at github.com/saebooks/saebooks. Fork it, run it, modify it.

AGPLv3 copyleft applies: if you distribute a modified binary, or run a modified version as a network service for others, you must publish your source changes under the same licence.

Can I run SAE Books commercially?

Yes — AGPLv3 permits commercial use. You can sell a service built on SAE Books, charge for installation, or integrate it into a paid product, provided you comply with the copyleft requirements.

What about the SAE Engineering relays (license, lodge, feeds)?

Those are private commercial services. A fork cannot use SAE Engineering's Stripe keys, ATO Machine Credential, or SISS Data Services agreement. Forkers who want bank feeds, STP/BAS lodgement, or licence-management infrastructure need to establish their own agreements with the relevant parties (ACSISS / SISS, ATO DSP program, Stripe).

Can I embed SAE Books in my product without releasing my code?

AGPLv3 requires source disclosure for network service use. If your product exposes SAE Books functionality over a network, your changes to the SAE Books source must be published. The AGPL licence does not require disclosure of your surrounding proprietary application code — only the modifications to SAE Books itself.

Architecture and integration

What is the recommended integration pattern?

For integrations that act on behalf of a user, use OAuth handoff — the user authenticates on the SAE Books side, your server gets a scoped token. For backend automations, create an integration user in the tenant with a long-lived rotating credential.

See Build an integration for the full walkthrough.

Is there a webhook API?

SAE Books accepts inbound webhooks from external services (e.g. Resend for email delivery events, Paperless-ngx for document ingestion). An outbound webhook surface — for notifying your endpoints when SAE Books records change — is not yet implemented; follow the project changelog for when it ships.

How do I get an OpenAPI client for my language?

The OpenAPI schema is at /openapi.json on every install. Feed it to your generator:

# Python (openapi-python-client)
pip install openapi-python-client
openapi-python-client generate --url https://app.saebooks.com.au/openapi.json

# TypeScript (openapi-typescript)
npx openapi-typescript https://app.saebooks.com.au/openapi.json -o schema.d.ts

What's the pagination model?

Offset-based on most list endpoints. GET /api/v1/invoices?limit=50&offset=0 returns {items: [...], total: <int>, limit: <int>, offset: <int>}. Increment offset by limit to page through results.

The change-log endpoint (GET /api/v1/changes) uses cursor-based pagination via the X-Cursor-Next response header; pass ?since=<cursor> on subsequent calls.

See Conventions for the full pagination spec.

How does idempotency work?

Include X-Idempotency-Key: <uuid> on any state-changing request. SAE Books stores the response and returns it verbatim on replay. Generate a fresh UUID per logical operation; reuse the same UUID to retry a failed request safely.

Can I query across tenants?

No, and that's by design. Every API call is scoped to the authenticated user's tenant. Cross-tenant queries are an operator-role code path that is explicitly audited — not available via the customer API.

Development

Where do I report bugs?

GitHub Issues on saebooks/saebooks. Include your SAE Books version (from /api/v1/version), the relevant API endpoint, and the minimal reproduction case.

Is there a community?

discourse.saebooks.com.au — developers, operators, and the SAE Books team. SAE Books uses its own DiscourseConnect implementation for forum SSO, so you can sign in with your SAE Books account.

How do I run the test suite?

git clone https://github.com/saebooks/saebooks.git
cd saebooks
bash scripts/run-tests.sh

The test suite spins up an isolated Postgres on a private Docker network, runs alembic upgrade head, then runs pytest. The live app container is built without dev dependencies — do not run pytest against it. The suite uses pytest-asyncio + httpx ASGITransport and requires no external services. See Testing for fixture conventions.

What Python version does SAE Books target?

Python 3.12+. The container image pins to 3.12-slim. pyproject.toml sets requires-python = ">=3.12".

How do I set up a dev environment outside Docker?

Most contributors don't bother — the compose setup is the dev environment. If you do want a bare-metal setup:

python3.12 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Start a local Postgres, set DATABASE_URL in .env
alembic upgrade head
uvicorn saebooks.main:app --reload --port 8042

What's the commit format?

One logical change per commit. Imperative subject line, ≤72 characters. Body explains why, not what — the diff shows what. See Contributing for the full guide.

Operations

How do I upgrade SAE Books?

docker compose pull
docker compose up -d
# Migrations run on startup

If a migration fails, the container exits non-zero. Read the logs before doing anything else. See Self-host in production for the full upgrade procedure.

Does SAE Books phone home?

Community edition: no. It does not contact SAE Engineering unless you configure an external service (lodge relay, feeds relay).

Business / Pro / Enterprise: the licence token refreshes daily from license.saebooks.com.au. No accounting data leaves your server — the refresh call sends only the current token (which contains the licence ID and edition, nothing more).

Where is data stored?

Postgres only. All state — journal entries, invoices, contacts, sessions, everything — is in the database. The app container is stateless; the Postgres data volume is the only thing you need to back up and restore.

How does multi-tenancy work for operators?

Multi-tenant mode is an Enterprise feature. Each tenant has its own tenant_id, and Postgres RLS policies enforce per-tenant data isolation at the database layer — not just at the application layer. The saebooks DB role has rolbypassrls=t (the owner/migration role); the saebooks_app role used by the running application has rolbypassrls=f (NOBYPASSRLS) so RLS policies bind it on every request. See Tenancy & RLS for the full picture.