Overview
apps/finance and the Finance experience inside apps/web are paired Finance
hosts. They must stay 1:1 for product features, data behavior, mutations,
permissions, route behavior, and user-facing workflow updates.
Canonical workspace routes include:
/{wsId}/{wsId}/transactions/{wsId}/wallets/{wsId}/invoices/{wsId}/categories/{wsId}/tags/{wsId}/recurring/{wsId}/budgets/{wsId}/analytics/{wsId}/debts
apps/web /{wsId}/finance/* routes render the same shared Finance product
surface inside the normal platform dashboard shell. apps/finance keeps the
shorter standalone route shape without the /finance segment.
Use route prefixes instead of forks:
apps/web:financePrefix="/finance"andFinanceRouteProvider prefix="/finance".apps/finance:financePrefix=""andFinanceRouteProvider prefix="".
/{wsId}/finance/transactions/categories, should redirect inside apps/web to
/{wsId}/finance/categories while preserving query strings.
API Ownership
apps/financeowns the standalone workspace shell, Finance UI route wrappers, local/verify-tokenhandoff, host-local auth/session routes, and build-info.apps/webowns platform-shell Finance route wrappers under/{wsId}/finance/*.apps/financeowns the hard-cutover product handlers for transactions and categories, wallets and checkpoints, budgets, debts, recurring transactions, invoices, charts and overview reporting, and Inventory reconciliation. It also has local supporting handlers for promotions, inventory products, settings, linked products/promotions, and workspace/user lookup needs.- The local recurring, invoice, and wallet examples include
/api/v1/workspaces/:wsId/finance/recurring-transactions,/api/v1/workspaces/:wsId/finance/invoices, and/api/workspaces/:wsId/wallets. Exact local handlers win before the/api/:path*fallback rewrite is considered. - Deliberately central exceptions remain in
apps/web: the legacy/api/v1/workspaces/:wsId/walletslist/create route, wallet-role whitelist routes, shared workspace storage routes used by transaction attachments, and the Finance exchange-rate cron. Unmatched requests reach Web through the fallback rewrite. - Keep shared callers on
@tuturuuu/internal-api. Finance-owned helpers use the Finance API base URL for server calls, while helpers for the deliberate Web exceptions remain on the central API origin; browser calls stay same-origin. - Finance-local protected handlers accept the Finance app-session actor and enforce route-level workspace and product permissions. Forwarded Web exceptions require the coordinated Web-issued session and retain their own authorization checks.
- Finance transaction attachment reads must authorize through the same
authenticated transaction visibility path as normal transaction reads. Do not
grant storage list, metadata, or signed-read URL access from coarse
view_transactionsorupdate_transactionspermissions alone; reuseget_wallet_transactions_with_permissionswithp_transaction_idsso wallet whitelists, viewing windows, and granular income/expense permissions stay aligned. - Finance transaction attachment uploads have server-side limits: 10 files per transaction and 50 MB per file. The signed-upload route must reject over-limit declared sizes before issuing a URL, and finalize must inspect the actual stored object size/count and delete over-limit uploads before any follow-up processing.
- Finance transaction type filters must not classify confidential amount signs
for callers without
view_confidential_amount. RPCs that acceptp_transaction_typeshould apply income/expense predicates only when the row is non-confidential or the caller can view confidential amounts; otherwise typed filters should omit those redacted rows instead of using the raw amount sign. - Finance invoice customer IDs must be validated against
workspace_userswith the route workspace before insert, and admin-backed invoice reads should resolve customer display fields with an explicitws_idfilter instead of a nested service-role join oncustomer_id.
apps/finance when it belongs to a hard-cutover family; preserve the explicit
Web exceptions until their dependent platform callers migrate. Expose shared
access in packages/internal-api and consume it through TanStack Query or
shared server components.
Inventory sales reconciliation
Finance owns the Inventory reconciliation inbox and its protected API under/api/workspaces/:wsId/finance/inventory-reconciliation. Access to pending
entries, provider mappings, provider history synchronization, manual
adjustments, and bulk link/unlink actions requires manage_finance.
Every provider-confirmed Polar, Square POS, or Square Terminal event is first
stored as an immutable private.inventory_finance_entries source row. The
source row affects balances only after an atomic database RPC links it to a
currency-compatible wallet transaction. Missing wallets stay pending and are
never included in income, wallet, or net-total calculations.
Provider-and-currency mappings take precedence over the Inventory revenue
wallet fallback and the Finance default wallet. A wallet must use the entry
currency. Category resolution uses the unanimous product category first, then
the provider mapping, the Inventory default, and finally uncategorized.
Deleting or explicitly unlinking a provider transaction removes only the
ledger row and returns the immutable source entry to pending. Provider,
reference, signed amount, and occurrence date remain provider-controlled;
wallet, category, tags, description, and confidentiality remain editable.
For rollout and recovery:
- Deploy the database migration before Inventory, Finance, or shared-package consumers.
- Verify historical completed provider sales appear in the pending inbox. The migration never auto-posts unmatched historical sales.
- Configure provider and currency mappings, inspect pending counts by currency, and only then use bulk linking.
- Run the bounded provider-history sync explicitly to discover historical refunds and Square disputes.
- If a ledger row was deleted accidentally, relink the still-present source entry. Do not recreate provider events manually.
- Use audited manual adjustments only when the provider has no supported event, such as a Polar chargeback.