Analytical Ledger is a small number of well-understood parts. This page names them, says where each one runs, and says what leaves your instance and when.
The pieces
| Part | What it is |
|---|---|
| The application | A Next.js 16 app with server components and server actions. Actions are validated with zod and return typed results. Read models are plain async functions that screens call directly. |
| The database | PostgreSQL 18. Drizzle owns the schema and migrations. A separate file of triggers, installed after the migrations, holds the integrity rules. |
| Money | NUMERIC(20,4) columns, decimal strings in the app, and one module, money.ts, for all arithmetic. |
| Sign-in | Auth.js sessions with password sign-in, or Google and Microsoft sign-in, and database-backed roles: viewer, accountant, owner. |
| The MCP endpoint | A stateless Streamable HTTP endpoint at POST /api/mcp with OAuth 2, for your own agent. |
| Documents | Uploaded files, stored under DOCUMENT_STORAGE_ROOT on the instance. |
How the same pieces run depends on how you run it.
| Way to run | Runtime |
|---|---|
| Windows Preview | A portable build with its own Node and an embedded PostgreSQL 18, both bound to your machine's loopback interface. One user, one computer. |
| Self-host Preview | The app on Node, against a PostgreSQL 18 database you manage. |
| Managed hosting Preview | Docker Compose per client: the app as a non-root, read-only image, PostgreSQL 18 on an internal network, and Caddy terminating TLS. |
One database per instance
An instance owns one database and one document root. Several entities live inside it, each with its own chart of accounts, periods and books. Consolidated views add the entities up and eliminate accounts flagged intercompany.
Entities are accounting scopes, not security domains. Anyone with access to the database can reach every entity in it. If two clients must not be able to see each other, they get two instances. That is the hosting model: one isolated instance and one database per client, and no shared tenant database.
The current entity or consolidation group is resolved on the server from the request, never hard-coded and never trusted from the browser.
How a request is checked
- The server resolves who you are from your session, and your role from the current database state, not from a claim in an old cookie.
- It resolves your view: the entity or group, and the fiscal year.
- It validates the input and checks that the role may take the action.
- The posting code checks balance, entity ownership and the open period.
- PostgreSQL checks them again, as the last word. See How double-entry is enforced.
Some endpoints do not use a browser session. Cron endpoints require bearer secrets, Stripe webhooks verify signatures, and the client-portal receivables seam verifies an HMAC over the raw request bytes with a single-use nonce. A browser session is not a substitute for those checks.
The MCP endpoint
An agent connects to POST /api/mcp with an OAuth access token. The token is bound to one owner, one set of entities and one fiscal year, and to one of two scopes.
- The server discovers its metadata at
/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource/api/mcp, and sends the owner to/oauth/authorizeto consent. - Authorization uses the code flow with S256 PKCE and dynamic client registration.
- Access tokens last one hour. Refresh tokens last 30 days and rotate.
- Calls are rate limited per token, requests from a foreign browser origin are refused, and the token must belong to an active owner.
- Every request is written to
mcp_access_audit. An agent can draft, never post, and that is enforced bytrg_assistant_approval_guard, not by the agent's good behaviour.
See Connect your agent for the scopes and the client steps.
What leaves the instance
Parsing a CSV, removing duplicates, doing the arithmetic, posting, reconciling, budgeting and building reports all run on your instance. A feature sends data out only when you have configured it.
| Connection | When | What it sends |
|---|---|---|
| Anthropic | AI categorization or extraction, or an Anthropic assistant lane you selected. | Prompts, chart and context, selected transaction descriptions and amounts, and document contents for extraction. |
| OpenAI | An OpenAI assistant lane you selected. | Prompts and allowed accounting tool results. |
| A local or custom model server | When you configure one. | The same, to an origin you choose. Loopback stays on the host; a remote origin leaves it. |
| Google or Microsoft sign-in | When someone signs in with it. | OAuth identity claims, including email. No balances. |
| SimpleFIN Bridge | A bank sync. | Your access URL as authentication, and account and date-range requests. The response holds your bank accounts and transactions. |
| Stripe | A payout query or sync. | API authentication and payout identifiers. |
| Tax1099 | An explicit e-file submission. | Payer and payee details, taxpayer IDs and amounts. |
| Resend | A digest or a statement pack by email. | Addresses, subject and the selected financial summaries. |
| Slack | A digest notification. | The digest, including financial alerts. |
| Your external agent | When it calls a tool you authorized. | Scoped tool responses, to the client you connected. |
Nothing in this table runs until you save the key or connect the client. Removing the credential or revoking the connection stops it. Exports, backups, logs and email packs are further copies that you control, so review them before you share them.
The source and its dependencies were reviewed for first-party product analytics and crash-upload code, and none was found. That is not a blanket no-telemetry claim: Next.js build tooling has its own telemetry, which NEXT_TELEMETRY_DISABLED=1 turns off, and your model providers, agents and host have their own policies.