1. Admin overview
This guide is for workspace owners and admins. It covers the controls that affect every user in the tenant — billing, branding, invites, and bank-connect setup.
- Owner — root account, manages billing, can demote / delete admins.
- Admin — manages users, branding, categories, and integrations.
- Accountant — full read/write on financial data, no admin controls.
- Viewer — read-only access to dashboards and reports.
2. Workspace & branding
The workspace is your tenant — the isolation boundary for every other resource. Branding carries through to every exported report.
- 01Open Settings → WorkspaceUpdate the workspace name. This appears as the title on every report PDF / Excel cover.
- 02Pick a brand colorHex color — used for emphasis blocks (Net Income, Total Liabilities + Equity, Net Change in Cash) and the reconciliation header.
- 03Upload a logoUp to 2 MB, base64-encoded and stored alongside your tenant record. The logo appears at the top of every PDF cover.
3. Users, roles & invites
Add accountants, bookkeepers, and stakeholders by invite. Every invite is single-use and expires.
- 01Open TeamSidebar → Team.
- 02Click 'Invite user'Pick a role (Admin, Accountant, Viewer). Owners cannot be invited — they are created at workspace signup.
- 03Share the invite linkCopy the generated URL and send it to the recipient. The link works once and self-expires after the configured TTL.
- 04Manage existing usersToggle role or deactivate from the team table. The owner cannot be deactivated.
4. Billing & plan limits
Billing is handled via Stripe Checkout. Plans differ in bank account count, seat count, and statement history depth.
- Starter — $29/mo: 1 bank account, 3 months history, 1 seat.
- Pro — $79/mo: 5 bank accounts, 60 months history, 5 seats.
- Business — $199/mo: unlimited accounts, 60 months, 50 seats.
- 01Open BillingSidebar → Billing.
- 02Click 'Upgrade plan'Stripe Checkout opens in a new tab. After payment, your plan and limits update automatically.
- 03Download invoicesPast payment_transactions are listed in the billing screen — click any row to view.
STRIPE_WEBHOOK_SECRETfrom backend/.env. In production (with a real sk_live_* key), any request missing the Stripe-Signature header is rejected with a 400, and any bad signature is caught by the SDK's construct_event. In dev mode (sk_test_emergent) signatures are accepted permissively — this is intentional so local proxy replays work. Set the real secret in .env and restart the backend before pointing production traffic at the webhook endpoint.5. Custom categories
The default category list covers most general-purpose businesses. Add domain-specific categories per workspace to match your chart of accounts.
- 01Open Settings → CategoriesReview the default list. Defaults cannot be removed but you can add custom ones below.
- 02Add a new category (Settings)Name and pick a group (Income, COGS, Expense, Other). The group decides which report bucket the category contributes to (e.g. an 'Income' custom category feeds Revenue on the P&L).
- 03Add a new category (Transactions page)Users can also create categories inline: on the Transactions page, click the + button next to the category filter, name the category, pick a group, save. It appears in every dropdown immediately and is tenant-scoped (every teammate sees it).
- 04Bulk re-categorize legacy dataUse the bulk re-categorize action on the Transactions screen. Select rows via checkboxes, pick the new category from the bulk-action bar, click Apply.
- 05Bulk import via CSVThe Transactions page 'Import CSV' button lets users paste or upload up to 500 rows with per-row category or a bulk 'Assign category' bar in the preview. Duplicates on (bank, date, description, amount) are auto-skipped. Column mapping is saved per bank in browser localStorage.
- 06Delete or bulk-delete rowsThe trash icon on any Transactions row (or the bulk-action bar's Delete button) removes the row(s) permanently. Reconciled rows are protected — un-reconcile first. Deleting also decrements the parent statement's counter.
6. Bank Connect setup (Teller MTLS)
Teller is the integration that lets your users link real bank accounts. It uses mutual TLS: your backend presents a client certificate and private key issued by Teller on every API call.
- 01Create a Teller applicationVisit teller.io → dashboard → create an application. Note the Application ID (looks like app_...) and download the certificate.pem + private_key.pem pair.
- 02Place the certsDrop the two .pem files into /app/backend/teller_certs/. The backend reads them at startup; restart after replacing them.
- 03Set environment variablesIn backend/.env, set TELLER_APP_ID, TELLER_ENVIRONMENT (sandbox/development/production), TELLER_CERT_PATH, TELLER_KEY_PATH, and (for webhooks) TELLER_SIGNING_SECRET.
- 04Verify the config endpointHit GET /api/bank-connect/config — it should return configured: true. If false, the message field tells you what is missing.
- 05Connect a bank from the UIOpen Bank Accounts and click 'Connect a bank'. In sandbox mode the password is always 'password'.
7. Platform admin (cross-tenant)
Platform admin is a SaaS-wide superuser role for Financials Ready staff. Anyone whose email is listed in PLATFORM_ADMIN_EMAILS (server env, comma-separated) gets is_platform_admin=true on startup, plus a "Platform admin" link in the sidebar.
- 8-KPI control room — total tenants, users, active subs, MRR, trial subs, bank accounts, statements, txns (last 30d).
- Searchable tenant table with inline editors for plan (starter/pro/business) and subscription status (active/trial/comped/suspended/canceled). The comped status gives a tenant Business-plan privileges without billing — useful for partner accounts and giveaways.
- Tenant detail drawer — click any row to open 4 tabs: Overview (KV + counts), Users (with platform-admin badges), Billing (Stripe transactions), Audit log (timeline of plan changes, impersonations, flag events).
- Login as tenant — mints a 15-minute JWT marked
impersonated=trueand switches you into the tenant's owner session. An amber banner persists at the top of the app while active; "Exit support session" pops you back to your platform-admin login. - Export tenant data — generates an AES-256 password-protected ZIP of every record scoped to the tenant (12 collections + manifest + README). Password returned in
X-Export-Passwordresponse header AND persisted in the audit log so a future admin can re-derive it. Used for GDPR Art. 15 / CCPA §1798.110 data-subject access requests.
admin_auditcollection with actor email + payload diff. The tenant detail drawer's Audit tab surfaces this timeline.8. Demo tenants & background crons
Financials Ready exposes a public zero-signup demo endpoint used by the landing-page "Try the live demo" button, plus two APScheduler cron jobs that keep the database healthy. This section is for platform admins who need to reason about these background writes.
- POST /api/auth/demo — unauthenticated. Mints a tenant with
id = demo-<12-hex>, an owner user (emaildemo-<12-hex>@financialsready.app, random 32-byte password), one bank account, one statement, and ~240 seeded transactions over 90 days. Returns the same{ token, user, tenant }payload as/auth/login. The tenant is placed on the business plan so all features are unlocked for the trial. - Rate limit: 5 demos per source IP per hour (sliding window, in-memory). Exceeding the limit returns HTTP 429 with a friendly retry-after message. Because the counter is in-process, the effective per-host limit is
5 × N-workers— fine for a preview button, but consider a Redis-backed limiter before a serious marketing launch. - Daily 3am UTC — dormant-user deactivation. Any non-owner, non-platform-admin user whose
last_login_atis older thanDORMANT_USER_DAYS(default 90) is flipped tois_active=false. Anadmin_auditrow with actionuser.auto_deactivateis written per user. Owners are exempt so a tenant is never accidentally locked out. - Daily 4am UTC — demo tenant cleanup. Every tenant whose
idstarts withdemo-and whosecreated_atis older than 24 hours is purged along with ALL child rows: users, bank accounts, statements, transactions, memberships, custom categories, bank connections, and prior audit entries. A finaldemo_tenant.cleanupaudit row is written for each purge. - Daily 5am UTC — recurring rules materialisation. Every active row in
recurring_ruleswhosenext_runis today or earlier is materialised into a realtransactionsrow (withis_manual=true) and the schedule is advanced by itscadence. If the currentnext_run'sYYYY-MMappears in the rule'sskip_periods, the row is advanced without creating a transaction (pause window). Duplicate protection: if a matching(bank, date, description, amount)row already exists (e.g. a statement upload created it first), the schedule still advances but no duplicate is inserted. Backfill loop caps at 24 iterations per rule to avoid runaway loops on malformed dates. - Weekly Monday 9am UTC — upload reminder push. Sends a web-push nudge to any owner whose tenant hasn't uploaded a statement in the last 30 days. Respects each user's push-subscription opt-in.
python -c "from routes_demo import cleanup_expired_demo_tenants; import asyncio; asyncio.run(cleanup_expired_demo_tenants())"from /app/backend to fire it on demand.secrets.token_urlsafe(32) — effectively unguessable. Demos are strictly session-bound: once the returned JWT expires or the tenant is purged, that user is inaccessible. This is intentional.9. Data lifecycle & deletes
Cascade rules to know about before you delete anything:
- Deleting a bank account deletes all related statements and transactions.
- Deleting a statement deletes all transactions that originated from it.
- Deleting a transaction is now surfaced directly on the Transactions screen — via a per-row trash icon or a bulk-action bar "Delete" button. The parent statement's
transaction_countis decremented automatically so exports and dashboards stay in sync. - Reconciled transactions are protected from deletion — the endpoint returns HTTP 409 and the UI disables the trash icon with a "Un-reconcile first" tooltip. This is a safety guard so a closed period never silently changes underneath you.
- Bulk delete silently skips reconciled rows in the selection and reports them via
skipped_reconciled— the un-reconciled rows are still deleted. - Deleting a user revokes their token and removes them from the team table; transactions they edited remain.
- Deleting a tenant (workspace) is a support-only action and cannot be self-served.
- Demo tenants (
idprefixeddemo-) are auto-purged after 24h by the daily 4am cron — see section 8.
10. Security & compliance
- Passwords are hashed with bcrypt at cost factor 12; we never store plaintext.
- JWTs are signed with HS256 using the per-deployment JWT_SECRET. Tokens expire after 24h of inactivity.
- Stripe API keys, Teller MTLS keys, and the LLM key live in backend/.env — never in client code.
- Tenant isolation is enforced at the database query layer: every read and write is filtered by tenant_id.
- Webhook signatures (Teller, Stripe) are verified before any state mutation occurs.
11. Troubleshooting
- Statements stuck in 'parsing' — check the backend supervisor logs for parser errors. Restart with
sudo supervisorctl restart backendif needed. - AI categorization returning 'Uncategorized' — your Emergent LLM key may be out of credit. Check Settings → Universal Key in the Emergent admin console.
- Teller Connect button disabled — usually a missing TELLER_APP_ID or missing cert files. Use GET /api/bank-connect/config to confirm.
- Stripe checkout opens but plan never updates — webhook signature failing. Check the webhook URL in Stripe matches your deployed backend AND that
STRIPE_WEBHOOK_SECRETin.envmatches Stripe's dashboard value. See section 4. - Statement preview says "No date column detected" — your CSV headers don't include a recognized date keyword. Cancel the preview, rename the column to
DateorPosted Date, re-export from your bank, and try again. - Trash icon disabled on a transaction — the row is reconciled. Toggle the reconciled checkbox off first, then the trash icon becomes active.
- "Try the live demo" returns 429 — you've exceeded the per-IP demo rate limit (5/hour). Wait, or bump
DEMO_RATE_LIMITinroutes_demo.py. See section 8. - PDF export looks wrong — try clearing branding (Settings → reset color & logo) to isolate whether the issue is brand-color or layout-related.