Skip to main content

Sahidha Agent Guide

This document is for AI agents (Claude, ChatGPT, Gemini, others) — or humans wiring up an agent — that want to operate Sahidha programmatically.

If you're a human looking for the web UI, ignore this and use https://app.sahidha.com. If you're an agent and you stop reading after the first three sections, that's still enough to do useful work.

note

The REST API at https://api.sahidha.com/api/v1 is live now and is the source of truth — see the API reference. The @sahidha/mcp and @sahidha/cli npm packages are in private beta (not yet on the public npm registry); the install commands below are how they'll work once published.

Architecture: one API, two optional skins

┌──────────────────────┐
│ Fastify REST API │ ← source of truth
│ /api/v1/… (62 eps) │ (RLS, audit, scopes)
└───────────┬──────────┘
│ Bearer ffp_…
┌─────────────────┴──────────────────┐
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ ff CLI │ │ MCP server │
│ apps/cli/ │ │ apps/mcp/ │
│ (humans) │ │ (AI agents) │
└───────────────┘ └────────────────────┘
shell, table, stdio JSON-RPC,
piped JSON typed tool schemas

Pick the right surface:

  • MCP server (apps/mcp/) → if you're an agent inside Claude Desktop, Cursor, Continue, Cline, or any MCP-aware host. Tools are typed, validated, discoverable. This is the recommended path for LLMs.
  • ff CLI (apps/cli/) → if you're a human at a terminal, or an agent with only shell access. Same capability set, friendlier for piping.
  • Direct REST (/api/v1) → if you're building your own integration (mobile app, custom dashboard). Authenticate with the same ffp_… token.

The REST API is the truth. Both skins are independent and optional — neither ships logic the other doesn't have. Choose based on your client, not your goals.


1. The shape of the system

Sahidha is a multi-tenant household-finance app. Every piece of data belongs to a family (the tenant). Inside a family there are members (human users), entity-members (a HUF, business, trust — non-person tenants), and optional cross-family guests (tax advisors who have read access into other families).

Everything else — accounts, transactions, insurance, investments, contacts, todos, reports — lives under a family. Tenant isolation is enforced at the database level via PostgreSQL Row-Level Security; an agent simply cannot read another family's data even if a route forgot to filter (defence in depth).

Roles

Every login has exactly one role per family. Agents inherit the role of the user whose token they use — there is no separate "agent" role.

RoleSeesCan change
family_adminEverything in the familyEverything, plus member & family administration
memberOnly their own accounts / dataTheir own data; can pay on-behalf for others
guestAll members' data (read-leaning)Add new transactions / imports / todos only — cannot edit or delete anything that already exists

Guest role — applicable rights

A guest is a read-leaning collaborator — typically a tax advisor or accountant. A guest exists only as a cross-family guest (Settings → Members → Cross-family guests): an admin invites an existing Sahidha user from another family by email; that person keeps their own login and switches into your family from the header chip. (The former in-family "guest member" role has been retired — role=guest on member create/update is now rejected; use the cross-family invite instead.)

A guest CAN:

  • Read everything in the family — every member's accounts, transactions, insurance, investments, transfers, contacts, todos, and all reports (unlike a member, who sees only their own data).
  • Add new transactions (including on-behalf entries) and import bank statements (parse + confirm).
  • Add todos.
  • Mint API tokens scoped to their own (guest) permissions.

A guest CANNOT:

  • Edit or delete any existing transaction or todo (no edit / delete / restore / bulk action / transfer-link).
  • Create, edit, or delete accounts, insurance policies, or investments.
  • Create or delete transfers, or link transfers / masters.
  • Modify contacts, or touch cross-payments / IOUs.
  • Excel export or import.
  • Open Settings — member management, family settings, categories, or any other admin-only route.

In short: a guest can contribute new entries and read the whole family's finances, but cannot alter or remove existing records or change any structure.


2. Quick start (agents — MCP)

The MCP server is published to npm as @sahidha/mcp. Hosts that speak MCP (Claude Desktop, Cursor, Continue, Cline) auto-discover and call its 62 tools.

Wire it into your host's config. For Claude Desktop, edit %APPDATA%\Claude\claude_desktop_config.json (Windows) or ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):

{
"mcpServers": {
"sahidha": {
"command": "npx",
"args": ["-y", "@sahidha/mcp"],
"env": {
"SAHIDHA_API_URL": "https://api.sahidha.com/api/v1",
"SAHIDHA_TOKEN": "sa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}

npx -y fetches the package on first launch and caches it; nothing to clone, nothing to build. For Cursor (~/.cursor/mcp.json) and Continue the shape is the same. Restart the host; you should see tools named sa_whoami, sa_accounts_list, sa_txns_create, etc.

Get a token from the web UI (Settings → API Tokens → New token, scope=write) or programmatically:

# Mint a fresh token for an agent (CLI, but the resulting token works for MCP).
ff auth login # one-time human login
ff tokens create --name "claude-desktop" --scope write --json
# → { "token": "ffp_...", … } ← capture once; never shown again

Verify tool surface from a host's tool picker — you should see 62 tools.

Pre-publish (workspace) alternative: if you're running from a git clone before the npm publish has landed, use the local path:

{
"command": "node",
"args": ["C:/path/to/Sahidha/apps/mcp/bin/sahidha-mcp.mjs"]
}

Hosted connector (Claude custom connector, read-only)

If you use Claude (web or desktop) and just want to read your family's finances, you don't need to install anything. Add the hosted connector:

Claude → Settings → Connectors → Add custom connector → URL https://mcp.sahidha.com/mcp

Claude walks you through an OAuth sign-in (your normal Sahidha email + password, or Google). It then has read-only access — the 18 list and report tools, and nothing that can create, edit, or delete. Two independent layers enforce that: only GET-equivalent tools are exposed, and the token the connector mints for itself is read-scoped. You can revoke it any time from Settings → API Tokens (it's named claude-connector).

Use the hosted connector for read-only Claude access; use the stdio @sahidha/mcp server below when you need full read/write from Claude Desktop, Cursor, Continue, or Cline with your own token.

3. Quick start (humans — CLI)

# Install globally — one command, no clone.
npm install -g @sahidha/cli

# Interactive login → mints + stores a token in ~/.sahidha/config.json (0600).
# Point at the hosted API (the default is localhost for local dev).
ff auth login --api https://api.sahidha.com/api/v1

# Smoke-test.
ff auth whoami --json
ff accounts list --json
ff txns list --from 2026-04-01 --to 2026-05-31 --json

For agents using the CLI fallback — always pass --json. The table view is lossy.

The CLI honours these env vars (override the config file):

Env varMeaning
SAHIDHA_API_URLBase URL (default http://localhost:3000/api/v1)
SAHIDHA_TOKENBearer token (replaces config-file value)
SAHIDHA_CONFIG_DIROverride ~/.sahidha location
FF_OUTPUT=jsonForce JSON output, equivalent to --json

4. Authentication

3.1 How to get a token

Three ways, pick the one that fits your situation:

  1. Interactiveff auth login prompts for email + password, mints a token, stores it locally. Best for one-shot human-driven setup.
  2. Web UI — sign in as a family_admin or member, open Settings → API Tokens, click "New token", choose scope, copy the plaintext (shown once). Best for granting an agent its own dedicated key.
  3. Programmatic — for tests or onboarding flows, you can POST to /auth/login yourself to get a session cookie, then POST /me/tokens to mint. See src/login.ts in the CLI for the exact pattern.

3.2 Token format and storage

  • Format: ffp_<43-char-base64url> (ffp = Sahidha Personal). Total length 47 chars. Grep-able prefix is intentional so leaked tokens are easy to find.
  • Stored server-side as SHA-256 hash. The plaintext is shown once at creation; if you lose it, revoke and mint a new one.
  • Scopes:
    • read — GET requests only. Read-only audits, reports, exports.
    • write — GET + mutations the issuing user can perform.
    • admin — Same as write but the issuer must be family_admin at mint time. Allows admin-only routes (member CRUD, audit truncate, etc.).
  • Expiry: optional. Pass --expires-days N at creation. No expiry by default — be deliberate about this for shared/agent keys.

3.3 Using the token

Pass it as a Bearer header to the API directly:

GET /api/v1/me HTTP/1.1
Authorization: Bearer ffp_abc123...

Or via the CLI, which adds the header for you (after ff auth login).

3.4 Revoking

ff tokens list --json # find the id
ff tokens revoke <id> # sets revokedAt; future calls 401

Or revoke from the web UI. Revocation is immediate — no propagation delay.

3.5 Multiple families

A user with cross-family guest memberships has one active family per session. Each API token is pinned to a single family at mint time and cannot switch. To act in a different family:

  1. Switch the web UI to that family (header chip).
  2. Mint a new token there.
  3. Use both tokens, one per family, in your agent.

5. CLI reference (every endpoint)

See ff --help and ff <cmd> --help for the up-to-date list. Below is the full surface, grouped by resource.

Naming conventions:

  • All flags are long-form (--foo value or --foo=value); boolean flags are --foo (true) or --no-foo (false).
  • <positional> is required, [--opt] is optional.
  • IDs are UUIDs unless noted. Dates are YYYY-MM-DD.
  • Amounts are positive numbers in INR. Sign is determined by --type.

4.1 Auth & tokens

ff auth login [--email E] [--password P] [--token ffp_…] [--api URL] [--name LABEL]
ff auth logout
ff auth whoami
ff auth config

ff tokens list
ff tokens create --name LABEL [--scope read|write|admin] [--expires-days N]
ff tokens revoke <id>

4.2 Me (current user)

ff me get
ff me password --current PW --new PW
ff me families
ff me set-active-family --family-id <uuid> # cookie sessions only; tokens are pinned

4.3 Family (admin)

ff family update [--name S] [--fy-start 1..12]
ff family members list
ff family members create --name N --email E --password P
[--role family_admin|member]
[--entity-type huf|business|trust|partnership|proprietorship|other]
[--managed-by <userId>]
ff family members update <id> [--role …] [--active true|false] [--password …]
ff family members delete <id> # blocked if owns data
ff family guests list
ff family guests invite --email E # email must match an existing user in another family
ff family guests revoke <userId>

4.4 Accounts

ff accounts list [--owner <userId>]
ff accounts create --name N --type savings|current|overdraft|loan|credit_card|fd|ppf|investment
--bank B [--number LAST4] [--opening N] [--credit-limit N] [--contact <id>]
ff accounts update <id> [--name] [--bank] [--number] [--credit-limit] [--owner] [--active] [--contact]
ff accounts delete <id> # blocked if any transactions exist

ff accounts statements <accountId> list
ff accounts statements <accountId> download <archiveId> [--out FILE]
ff accounts statements <accountId> delete <archiveId>

4.5 Transactions (alias: txns)

ff txns list [--account <id>] [--owner <userId>]
[--type expense|income|investment|insurance|transfer]
[--category <id|"null"|"any">]
[--from YYYY-MM-DD] [--to YYYY-MM-DD] [--q "search text"]
[--include-deleted] [--deleted-only]
[--link-id <id> --link-type insurance|investment]

ff txns create --account <id> --date YYYY-MM-DD --description S
--type expense|income|investment|insurance|transfer
--amount N
[--category <id>] [--ref S]
[--on-behalf --beneficiary <userId>]
[--link-type insurance|investment --link-id <id>]

ff txns update <id> [--description] [--ref] [--category] [--date]
[--debit] [--credit] [--type expense|income]

ff txns delete <id> # soft-delete (transfer pair: hard-delete both legs)
ff txns restore <id> # undo soft-delete
ff txns transfer-candidates <id> [--window-days N]
ff txns link-transfer <id> --counterparty <txnId>
ff txns link-master <id> --type investment|insurance --id <masterId>

ff txns bulk --action recategorize|delete --ids id1,id2,id3 [--category <id|"null">]

4.6 Categories (admin for writes)

ff categories list # active + inactive, parent + children
ff categories create --name N --kind income|expense [--itr HEAD] [--parent <id>]
ff categories update <id> [--name] [--itr] [--parent] [--active true|false]
ff categories delete <id> # soft-deletes if any txn uses it

4.7 Insurance & attachments

ff insurance list [--owner <userId>]
ff insurance create --name N --insurer S --type Life|Term|Health|Accident|Home|Motor|Travel
--number S
[--sum-assured N] [--premium N]
[--frequency monthly|quarterly|yearly]
[--next-due YYYY-MM-DD] [--nominee S] [--start YYYY-MM-DD]
[--linked-account <id>] [--contact <id>]
ff insurance update <id> [--name] [--insurer] [--premium] [--next-due] [--owner]
ff insurance delete <id> # blocked if any txn linked

ff insurance attachments <policyId> list
ff insurance attachments <policyId> upload --file PATH # PDF / image, ≤10 MB
ff insurance attachments <policyId> download <attId> [--out FILE]
ff insurance attachments <policyId> delete <attId>

4.8 Investments

ff investments list [--owner <userId>]
ff investments create --name N --type Mutual_Fund|Fixed_Deposit|PPF|Stocks|Bonds|NPS|Gold|Real_Estate
--folio S
[--invested-amount N] [--current-value N] [--monthly-sip N]
[--start YYYY-MM-DD] [--maturity YYYY-MM-DD] [--rate N]
[--linked-account <id>] [--contact <id>]
ff investments update <id> [--name] [--current-value] [--monthly-sip] [--owner]
ff investments delete <id> # blocked if any txn linked

4.9 Transfers

ff transfers list
ff transfers create --from <accountId> --to <accountId> --date YYYY-MM-DD --amount N [--note S]
ff transfers delete <groupId> # reverses both legs

4.10 Cross-payments / IOUs (alias: cp)

ff cross-payments list
ff cross-payments create --beneficiary <userId> --amount N --description S
[--category S] [--date YYYY-MM-DD]
ff cross-payments update <id> [--description] [--category] [--date] [--amount] [--beneficiary] [--payer]
ff cross-payments delete <id>
ff cross-payments acknowledge <id> # called by the beneficiary
ff cross-payments settle <id> --account <settlementAccountId>

4.11 Contacts

ff contacts list [--kind rm|insurance|investment|tax|legal|medical|contractor|other] [--include-inactive]
ff contacts create --name N [--kind …] [--org] [--phone] [--email] [--address] [--notes]
ff contacts update <id> [--name] [--kind] [--org] [--phone] [--email] [--address] [--notes] [--active]
ff contacts delete <id>

4.12 Todos

ff todos list [--status open|done] [--assigned-to <userId>]
ff todos create --title T [--notes] [--due YYYY-MM-DD] [--assigned-to <userId>] [--visibility family|private]
ff todos update <id> [--title] [--notes] [--due] [--assigned-to] [--visibility] [--completed true|false]
ff todos delete <id>

4.13 Imports (bank statements)

ff imports parse --file PATH --type csv|xlsx|pdf
[--password PW] # only for encrypted PDFs
[--account <id>] # required for header-rememberment
[--header-row N]

# The output contains `archiveId`, `headers`, `rows`, and an optional `hint`
# (per-bank column mapping remembered from a prior successful import).
# Persist the rows to disk, edit if needed, then confirm:

ff imports confirm --rows-file PATH.json --account <id>
[--archive-id <id>] # echo from parse output
[--mapping '{"date":"Date","debit":"Withdrawal",…}']
[--swap-direction true|false]
[--header-row N]

Passwords are sent over HTTPS in the body, used once to decrypt, never logged or returned in errors, and never stored.

4.14 Excel

ff excel export [--out FILE] # downloads all data as one .xlsx
ff excel import --file PATH # uploads + routes rows by sheet name

4.15 Reports

ff reports income-expenditure [--fy YYYY-YY] [--owner <userId>]
ff reports account-statement --account <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
ff reports credit-cards [--owner <userId>]
ff reports net-worth [--fy YYYY] [--fy-start-month 1..12] # admin-only

4.16 Invitations (admin)

ff invitations create --email E [--role family_admin|member] # guests: use the cross-family guest invite, not this
# Returns { token, url, expiresAt } — the invitee uses the URL to set their password.

4.17 Audit (admin)

ff audit export [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--out FILE]
ff audit truncate --before YYYY-MM-DD

4.18 Raw HTTP escape hatch

For anything not yet wrapped, or for novel orchestration:

ff request GET /reports?type=net-worth
ff request POST /transactions --body '{"accountId":"…","amount":100,…}'
ff request POST /transactions --body-file txn.json

Path prefix /api/v1 is normalised — pass it or omit it, both work.


6. Common workflows

5.1 Reconcile a bank statement end-to-end

# 1. Parse — preview rows; do NOT auto-confirm.
ff imports parse --file ./statement-HDFC-may.pdf --type pdf --account "$ACC" --json > parsed.json

# 2. Inspect. Each row has { date, description, debit, credit, suggestedType }.
jq '.rows[:5]' parsed.json

# 3. Edit if needed (typical: drop a header-page artifact row).
# Then write back the cleaned rows:
jq '.rows' parsed.json > rows.json

# 4. Confirm.
ff imports confirm --rows-file rows.json --account "$ACC" \
--archive-id "$(jq -r .archiveId parsed.json)" --json
# Find the investment id.
INV=$(ff investments list --json | jq -r '.[] | select(.folioNumber=="HDFC123") | .id')

# Add the transaction.
ff txns create --account "$ACC" --date 2026-05-01 \
--description "HDFC Top 100 SIP" --type investment --amount 5000 \
--link-type investment --link-id "$INV" --json

5.3 Pull a quarterly Income & Expenditure summary

ff reports income-expenditure --fy 2025-26 --json |
jq '{income: .totalIncome, expense: .totalExpense, net: .net}'

5.4 Bulk-recategorise all "Amazon" transactions

IDS=$(ff txns list --q "amazon" --json | jq -r '[.[].id] | join(",")')
ff txns bulk --action recategorize --ids "$IDS" --category "$SHOPPING_CAT_ID" --json

5.5 Snapshot a family's net worth nightly

ff reports net-worth --fy 2025 --json >> ~/snapshots/networth-$(date +%F).json

7. JSON I/O shapes

This section sketches the wire shapes the most-used endpoints. For the rest either use --json and inspect, or read the corresponding route handler in apps/api/src/routes/.

6.1 Common envelope

Successful responses are always JSON. Failures use the same content-type with HTTP status >=400 and shape:

{ "error": "human-readable message" }

Binary endpoints (/excel/export, /audit/export, attachment downloads, statement-archive downloads) return raw bytes with an application/... content-type plus a Content-Disposition: attachment; filename="…" header. The CLI saves to disk; the API SDK returns a Blob.

6.2 Transaction (read shape)

{
"id": "uuid",
"accountId": "uuid",
"txnDate": "2026-05-12",
"description": "Salary credit",
"refNumber": null,
"categoryId": "uuid|null",
"entryType": "income",
"debit": 0,
"credit": 250000,
"linkType": null,
"linkId": null,
"crossPaymentId": null,
"transferGroupId": null,
"source": "manual",
"financialYear": "2026-27",
"isReconciled": false,
"isDeleted": false,
"notes": null,
"createdAt": "ISO",
"updatedAt": "ISO"
}

6.3 Account

{
"id": "uuid",
"name": "HDFC Savings",
"accountType": "savings",
"bankName": "HDFC Bank",
"accountNumberMasked": "****4567",
"ownerUserId": "uuid",
"openingBalance": 100000,
"currentBalance": 152340.50,
"creditLimit": null,
"contactId": "uuid|null",
"isActive": true
}

6.4 Income & Expenditure report

{
"type": "income-expenditure",
"fy": "2025-26",
"income": { "Salary": [0,0,250000,...], "Other": [...] }, // 12 months, FY order (Apr-start)
"expense": { "Rent": [...], "Groceries": [...] },
"incomeIds": { "Salary": "uuid", "Other": null },
"expenseIds": { "Rent": "uuid", "Groceries": "uuid" },
"totalIncome": 1234567,
"totalExpense": 765432,
"net": 469135
}

6.5 Net worth report

{
"type": "net-worth",
"assets": 12_345_678,
"liabilities": 234_567,
"netWorth": 12_111_111,
"breakdown": { "bankAssets": 9_000_000, "investmentAssets": 3_000_000, "lifeAssets": 345_678, "liabilities": 234_567 },
"compare": {
"lastFyLabel": "FY 2024-25",
"lastFyEnd": "2025-03-31",
"thisFyLabel": "FY 2025-26",
"thisFyEnd": "2026-03-31",
"bankAssets": { "last":, "now":, "diff":},
"investmentAssets": { "last":, "now":, "diff":},
"lifeAssets": { "last":, "now":, "diff":},
"liabilities": { "last":, "now":, "diff":},
"assets": { "last":, "now":, "diff":},
"netWorth": { "last":, "now":, "diff":},
"snapshotNote": "Investment current value and Life-policy value aren't snapshotted historically; the same value is shown for both periods."
}
}

8. Error model & exit codes

The CLI maps HTTP status to shell exit code so scripts can branch:

ExitMeaning
0Success
1Generic error / network failure / bad input
2Unknown command / argv parse failure
3401 unauthorised — token missing or invalid
4403 forbidden — wrong role or wrong scope
5404 not found
6409 conflict — duplicate or constraint violation
7429 too many requests (reserved)
85xx server error

With --json, errors are emitted to stderr as { error, status, body }.


9. Safety guardrails for agents

  1. Always read before you write. Verify the target's state with a list or get before issuing PATCH / DELETE. The API is idempotent for some operations (txns delete on an already-deleted txn is a no-op) but not for others (e.g. transfers create is not — it would create a duplicate).
  2. Never log the token plaintext. Treat SAHIDHA_TOKEN like a password. Logs uploaded to ChatGPT / Claude transcripts are not safe storage.
  3. Don't bulk-delete from a regex. If you're operating on a list, print the candidates first and ask the user to confirm — ff txns list --q … then ff txns bulk --action delete --ids …. Same for accounts.
  4. Watch the FY boundary. Reports and many filters are FY-aware (Apr-start by default; configurable per family via financialYearStartMonth). ff txns list without --from/--to returns only the current FY.
  5. Statement uploads dedup by content hash. Re-uploading the same file does not duplicate transactions — the archive row updates in place and the parse-confirm flow re-checks duplicateHash per transaction.
  6. Imports require human review. Parsed rows aren't transactions yet. Always inspect, edit if necessary, and only then call imports confirm.
  7. Cross-family guest mode is read-leaning. Even with a write token, a guest cannot edit accounts / insurance / investments / settings. Plan agent capabilities around that.
  8. Audit logs are permanent. Every action via API token is logged with the token's name. Mutations cannot be hidden — agents should assume their behaviour is reviewed.
  9. No financial-transaction execution. This app only records money movements. Nothing here initiates a bank transfer or trade. If a user asks an agent to "send the rent," instruct them to do it in their bank app and then record it with ff txns create.

10. MCP tool reference

The MCP server exposes 62 tools. All names are prefixed fh_ (Sahidha) so they're easy to spot in a host's tool list. Grouped by resource:

GroupTools
identitysa_whoami, sa_families
tokenssa_tokens_list, sa_tokens_create, sa_tokens_revoke
accountssa_accounts_list/create/update/delete, sa_accounts_statements_list/delete
transactionssa_txns_list/create/update/delete/restore, sa_txns_transfer_candidates,
sa_txns_link_transfer, sa_txns_link_master, sa_txns_bulk
categoriessa_categories_list/create/update/delete
insurancesa_insurance_list/create/update/delete, sa_insurance_attachments_list/delete
investmentssa_investments_list/create/update/delete
transferssa_transfers_list/create/delete
cross-paysa_cross_payments_list/create/acknowledge/settle/delete
contactssa_contacts_list/create/update/delete
todossa_todos_list/create/update/delete
importssa_imports_parse, sa_imports_confirm
reportssa_reports_income_expenditure, sa_reports_account_statement,
sa_reports_credit_cards, sa_reports_net_worth
family/adminsa_family_members_list, sa_family_guests_list/invite/revoke, sa_invitations_create
rawsa_request (escape hatch for any endpoint above)

Each tool has a JSON Schema for inputs that the host validates BEFORE calling. Look up the exact shape via tools/list, or read apps/mcp/src/tools.ts — each entry is { name, description, inputSchema, run }.

Binary endpoints (excel export, statement download, attachment download) return a binary-descriptor object instead of streaming bytes through the MCP channel — agents that need the actual file should fall back to the CLI or call the REST API directly with the same ffp_… token.


11. Where to look next

QuestionFile
What endpoints exist?apps/api/src/routes/*.ts
What's the wire shape of X?The route's response-builder code
What does Y validate against?packages/api-contract/src/*.ts (Zod schemas)
How does auth resolve a token?apps/api/src/plugins/auth.ts
How is family scope enforced?packages/db/prisma/rls/*.sql (Row-Level Security)
How do I add a new command to the CLI?Append to the array in apps/cli/src/commands.ts
How do I add a new MCP tool?Append to the array in apps/mcp/src/tools.ts
Why did my mutation fail with 403?Check role + token scope. Run ff auth whoami.

12. Versioning

This guide is versioned alongside the API. Breaking changes will:

  1. Bump the API to /api/v2 and keep /api/v1 running for two releases.
  2. Update this file in the same commit.
  3. Note the change in CHANGELOG.md (forthcoming).

Until v1.0.0, additive changes only on /api/v1. Token format is stable.