* feat: prompt to activate missing BAS accounts at commit
Booking to an account not in the active chart previously threw a
generic 400 "Account(s) not found: 5010" and the user had to leave
the form to enable the account via /bookkeeping > BAS-katalog.
- New AccountsNotInChartError thrown from resolveAccountIds in the
engine (and the parallel resolver in core/storno-service). The
query also now filters on is_active=true, so deactivated accounts
are treated the same as never-added ones.
- API routes that call the engine (journal-entries, reverse, correct,
transactions/book + match-invoice + match-supplier-invoice +
uncategorize, invoices/mark-paid, supplier-invoices + mark-paid +
credit, salary/runs/correct, import/opening-balance/execute,
pending-operations/commit) catch the typed error and return a
structured 400: { error: { code: ACCOUNTS_NOT_IN_CHART,
account_numbers, message } }.
- /api/bookkeeping/accounts/activate now also reactivates rows that
already exist but are is_active=false, not only INSERTs. Returns
{ activated, reactivated, skipped, unknown }.
- New GET /api/bookkeeping/accounts/bas-lookup?numbers=... resolves
BAS names client-side so the dialog can show "5010 · Lokalhyra"
without bundling the full 1,276-account catalog.
- ActivateAccountsDialog lists the missing accounts (BAS names + any
unknown non-BAS numbers) and confirms with a single action.
- useSubmitWithAccountActivation wraps an async submit: on
ACCOUNTS_NOT_IN_CHART it opens the dialog, activates on confirm,
then retries the original submit so the user never re-enters data.
- AccountCombobox accepts any 4-digit numeric value, not just items
from the active chart — the activation dialog handles the rest.
- JournalEntryForm wired to the hook + dialog. Other submit surfaces
now surface a clear Swedish message ("Följande konton behöver
aktiveras: …") via getErrorMessage; wiring the dialog into those
is an additive follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: sync CLAUDE.md with current codebase state
Catch-up on growth since the last CLAUDE.md revision:
- Integrations list now includes AWS Bedrock, Upstash Redis,
Google Drive, Recharts, PDF.js, @react-pdf/renderer, xlsx,
fuse.js, ics.
- Extension table reflects cloud-backup enabled; adds
inbox-smart-match and example-logger; reorders to match current
extensions.config.json.
- Updated counts: 36 event types (was 30+), 35 MCP tools (was 26),
~60 tables (was ~47), 118 migrations (was 93), 19 report
endpoints (was 16), 20 report generators (was 17).
- lib/ directory table now covers salary, providers,
company-lookup, processing-history, support.ts; removes the
deleted settings/ subdir.
- App routes table adds /salary/*, /help, /settings/salary,
/settings/backup.
- API endpoints table adds /api/salary/*, /api/support/contact,
/api/account/delete, /api/audit-trail/*, /api/log,
/api/currency/rate, top-level extension routes.
- Tables section adds Salary, Third-party providers, Inbox &
Migration groups; removes salary_payments (replaced by
salary_runs + salary_line_items).
- Skills list updated to enumerate the Swedish domain skills by
name instead of the old single /swedish-bookkeeping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback on account activation
Seven fixes based on Greptile + Swedish compliance review on #308.
- ActivateAccountsDialog: disable the confirm button when any
entered number isn't a valid BAS account. Previously activation
would succeed for the knowns and the retry would immediately
fail again on the unknowns, giving a confusing double-toast UX.
- pending-operations/commit: revert commitSendInvoice and
commitMarkInvoiceSent to swallow AccountsNotInChartError
silently. The prior PR upgrade made these blocking, which
regressed invoice delivery for users whose AR accounts are
inactive — and since the activation dialog isn't wired into
those flows yet, there's no one-click recovery. The silent
catches now append an InvoiceJournalEntrySkipped event to
processing_history so the missing verifikation is actionable
in audit trails rather than silently understating the
momsdeklaration (revenue / utgående moms unposted).
- engine.reverseEntry: resolve account IDs with includeInactive=true
so storno of an already-committed entry goes through even when
the user has since deactivated one of its accounts. Blocking
the reversal would leave the original entry uncorrected in
violation of BFL 5 kap 5§ (rättelse must be documented). The
default (includeInactive=false) still applies to createDraftEntry
so new bookings to inactive accounts continue to trigger the
activation dialog.
- supplier-invoices POST + credit: roll back the just-inserted
supplier_invoices row (items cascade-delete) on any JE failure,
not only AccountsNotInChartError. An orphan supplier_invoices
row without a registration / credit JE leaves leverantörsskuld
(2440) and ingående moms (2641) unposted — a silent
understatement / overstatement in the momsdeklaration (ML
2023:200 / BFL 5 kap). The catch now returns a clear Swedish
error message for non-activation failures (typically period
lock or DB error) instead of silently logging.
Test mocks for chart_of_accounts updated for the new query chain
(eq.in.eq instead of eq.eq.in after the is_active conditional).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
33 KiB
CLAUDE.md — gnubok
Project Overview
gnubok is a Swedish-focused accounting SaaS for sole traders (enskild firma) and limited companies (aktiebolag). It implements double-entry bookkeeping compliant with Swedish accounting law (Bokforingslagen), including VAT handling, tax reporting, and 7-year document retention. Multi-tenant: each user can own or be a member of multiple companies, optionally grouped into teams (for consultants).
Tech stack: Next.js 16.1.5 (App Router), React 19.2.3, TypeScript 5 (strict), Zod 4, Supabase (PostgreSQL + RLS + email/password + TOTP MFA auth), Tailwind CSS 4 + shadcn/ui, Vercel hosting, Docker (self-hosted).
Integrations: Enable Banking (PSD2), TIC Identity (company lookup), Anthropic SDK, AWS Bedrock (@aws-sdk/client-bedrock-runtime for inbox smart-match), OpenAI (embeddings), Resend (email), Sentry (error tracking), Svix (webhooks), web-push (notifications), Upstash Redis + Ratelimit, Google Drive (cloud backup via OAuth), JSZip (archive export), sharp (image processing), Framer Motion (animations), Recharts (charts), PDF.js (pdfjs-dist), @react-pdf/renderer (invoice PDFs), xlsx, fuse.js (fuzzy search), ics (iCal feeds).
Path alias: @/* maps to the project root. Language: All code, comments, and commit messages in English. License: AGPL-3.0-or-later.
Commands
npm run dev # Start dev server (runs setup:extensions first)
npm run build # Production build (runs setup:extensions first)
npm run lint # ESLint
npm test # Run all Vitest tests
npx vitest run <dir> # Run tests in a specific directory
npm run setup:extensions # Regenerate extension registry from extensions.config.json
Key Architectural Relationships
- Multi-tenant model:
companiestable owns all business data.company_memberslinks users to companies with roles (owner/admin/member/viewer).teamsgroup companies for consultants. Company context resolved via cookie (gnubok-company-id) in middleware (lib/supabase/middleware.ts). - All journal entry creation routes through
lib/bookkeeping/engine.ts. Lifecycle:createDraftEntry()→commitEntry()(atomic voucher assignment viacommit_journal_entryDB RPC). Convenience:createJournalEntry()does both. Reversal viareverseEntry(). Correction viacorrectEntry()inlib/core/bookkeeping/storno-service.ts. - API routes that emit events must call
ensureInitialized()(fromlib/init.ts) at module level. This loads extensions, wires event handlers, and registers the supplier invoice handler + event log handler. - Event bus (
lib/events/bus.ts) is a module-level singleton. Handlers run viaPromise.allSettled— failing handlers never crash the emitter. 36 event types defined inlib/events/types.ts. The event log handler persists actionable events toevent_logtable for external automation. - Supabase clients: browser (
lib/supabase/client.ts), server with cookies (createClient()fromserver.ts), service role (createServiceClient()), cookieless service role for API key auth (createServiceClientNoCookies()fromlib/auth/api-keys.ts). Pagination helper:fetchAllRows()inlib/supabase/fetch-all.ts. - Extension system: Opt-in via
extensions.config.json. Core builds and runs with zero extensions. Currently enabled:enable-banking,email,arcim-migration,tic,mcp-server,cloud-backup. - Core reports (in
lib/reports/, not extensions): balance sheet, income statement, trial balance, general ledger, AR/supplier ledger, AR/supplier reconciliation, VAT declaration, journal register, monthly breakdown, continuity check, opening balances, KPI (+ definitions), NE-bilaga, INK2 declaration, SIE export, full archive export, salary journal, vacation liability, avgifter basis (employer contributions). - Types: All shared types in
types/index.ts(~2,570 lines, single source of truth). Import viaimport type { T } from '@/types'. Event types live inlib/events/types.ts. Extension types inlib/extensions/types.ts. - Error messages:
lib/errors/get-error-message.tsmaps technical errors to Swedish user messages (Zod → Postgres → HTTP → context fallback).
Multi-Tenant Architecture
Data Model
- companies: Business unit (name, org_number, entity_type, created_by, team_id). All business data (journal entries, invoices, transactions, etc.) has a
company_idcolumn. - company_members: Links users to companies (company_id, user_id, role, source='direct'|'team'). Roles:
owner,admin,member,viewer. - teams: Consultant grouping (name, created_by). A company can belong to one team. Team members auto-sync to company_members via DB triggers.
- team_members: Links users to teams (team_id, user_id, role='owner'|'admin'|'member').
- user_preferences: Stores
active_company_idper user.
Company Context Resolution
Middleware (lib/supabase/middleware.ts) resolves the active company on every request:
- Check
gnubok-company-idcookie - Fall back to
user_preferences.active_company_id - Fall back to first company membership
RLS policies use user_company_ids() DB helper function to filter by companies the user has access to.
Invitations
- company_invitations: Email-based invites with
gnubok_inv_prefixed tokens (SHA-256 hashed, 7-day TTL). - team_invitations: Same pattern for team invites.
- Token generation:
lib/auth/invite-tokens.ts.
Authentication
Supabase Auth with email+password (primary) and magic link (fallback). MFA via TOTP is supported.
MFA is enforced application-side (middleware + API routes), not in RLS policies. Controlled by two env vars:
NEXT_PUBLIC_SELF_HOSTED=true→ MFA never enforced (users can enable voluntarily)NEXT_PUBLIC_REQUIRE_MFA=true(hosted/Vercel) → middleware redirects to/mfa/enrollor/mfa/verifyuntil AAL2
API route auth (lib/auth/require-auth.ts): requireAuth() returns { user, supabase, error } discriminated union, enforces MFA on hosted.
API keys (lib/auth/api-keys.ts): SHA-256 hashed with gnubok_sk_ prefix. Scoped permissions (TOOL_SCOPE_MAP). Rate limited at 100 RPM via atomic DB RPC (validate_and_increment_api_key).
Cron auth (lib/auth/cron.ts): verifyCronSecret() with constant-time comparison.
Core Bookkeeping Engine
The engine (lib/bookkeeping/engine.ts) is the most critical system. All accounting flows route through it.
Lifecycle: createDraftEntry() → commitEntry() (atomic voucher assignment via commit_journal_entry DB RPC). Convenience: createJournalEntry() does both in one call. Reversal via reverseEntry() (storno). Correction via correctEntry() in lib/core/bookkeeping/storno-service.ts.
Key engine files:
transaction-entries.ts— Journal entries from bank transactionsinvoice-entries.ts— Journal entries from customer invoices (generatePerRateLines()for mixed-rate)supplier-invoice-entries.ts— Journal entries from supplier invoicesvat-entries.ts— VAT-related entriescurrency-revaluation.ts— Multi-currency revaluationmapping-engine.ts— Account mapping rules evaluationbooking-templates.ts/counterparty-templates.ts— Reusable templatespropose-payment-lines.ts/propose-send-lines.ts— AI-powered matching proposalshandlers/supplier-invoice-handler.ts— Event handler creating registration entries on confirmation
BAS data (bookkeeping/bas-data/): Full BAS 2026 chart organized by class (1–8) + SRU mapping.
Key BAS Accounts
1510 Accounts receivable | 1930 Business bank account | 2013 Private withdrawals (EF) | 2440 Accounts payable | 2611/2621/2631 Output VAT 25%/12%/6% | 2641 Input VAT | 2645 Calculated input VAT (EU) | 2893 Shareholder loan (AB) | 3001/3002/3003 Revenue 25%/12%/6% | 3305/3308 Export/EU service revenue
VAT Treatments
standard_25, reduced_12, reduced_6, reverse_charge, export, exempt
Invoice items support individual vat_rate values (mixed-rate invoices). Use getAvailableVatRates(customerType, vatNumberValidated) from lib/invoices/vat-rules.ts. VIES validation via lib/vat/vies-client.ts.
VAT Declaration Rutor (SKV 4700)
The VatDeclarationRutor type maps to the Swedish tax authority's momsdeklaration form:
- Ruta 05: Momspliktig försäljning — total domestic taxable sales (all rates combined, from 3001+3002+3003)
- Ruta 06/07: Unused (momspliktiga uttag / vinstmarginalbeskattning), always 0
- Ruta 10/11/12: Utgående moms 25%/12%/6% — output VAT per rate (from 2611/2621/2631)
- Ruta 39/40: EU services / Export (from 3308/3305)
- Ruta 48: Ingående moms — input VAT (from 2641/2645)
- Ruta 49: Moms att betala/återfå = (ruta 10 + 11 + 12 + 30 + 31 + 32 + 60 + 61 + 62) - ruta 48
Core Services (lib/core/)
bookkeeping/period-service.ts— Fiscal period lifecycle management (open, close, lock)bookkeeping/year-end-service.ts— Year-end closing proceduresbookkeeping/storno-service.ts— Reversal/correction entry generationtax/tax-code-service.ts— Tax code definitions and ratesaudit/audit-service.ts— Audit trail and compliance loggingdocuments/document-service.ts— Document attachment lifecycle (WORM storage with version chains)
Accounting Guard Rails
These rules exist for legal compliance, enforced by database triggers. Never violate them.
- Committed entries are immutable. Once
status: 'posted', cannot be edited or deleted (DB trigger). - Never delete posted entries. Use
reverseEntry()(storno) to cancel. - Every entry must balance.
sum(debits) === sum(credits), both> 0. - Voucher numbers are sequential. Assigned atomically via
commit_journal_entryDB RPC. Never set manually. - Voucher gap documentation. BFNAR 2013:2 requires documented explanations for gaps (
voucher_gap_explanationstable,detect_voucher_gapsRPC). - Period lock enforcement. DB trigger blocks writes to closed/locked periods. Company-wide lock date enforced via
enforce_company_lock_date()trigger. - 7-year document retention. DB triggers prevent deletion of documents linked to posted entries.
- Storno, never edit. Use
correctEntry()fromlib/core/bookkeeping/storno-service.ts. - Use
Math.round(x * 100) / 100for monetary calculations. NevertoFixed(). - Always use engine functions. Never insert directly into journal tables.
- Account numbers are strings.
'1930', never1930.
Extension System
Extensions are opt-in plugins in extensions/general/<name>/, controlled by extensions.config.json. Core builds and runs with zero extensions. npm run setup:extensions generates static imports in lib/extensions/_generated/ (runs automatically via predev/prebuild). Extensions cannot use dynamic imports (Next.js bundling).
Available Extensions (12)
| Extension | Purpose | Currently Enabled |
|---|---|---|
enable-banking |
PSD2 bank sync via Enable Banking | Yes |
email |
Email delivery via Resend | Yes |
arcim-migration |
Legacy ARCIM system data migration | Yes |
tic |
TIC Identity company lookup (org number → name, VAT, address) | Yes |
mcp-server |
MCP server for Claude Desktop/Code | Yes |
cloud-backup |
Google Drive backup of SIE + receipts + processing history | Yes |
inbox-smart-match |
AWS Bedrock AI matching of inbox receipts to bank transactions | No |
invoice-inbox |
Email-based invoice document processing | No |
push-notifications |
Web push notifications for events | No |
calendar |
Payment calendar with iCal feed | No |
skatteverket |
Skatteverket VAT declaration submission | No |
example-logger |
Reference implementation — logs events to console (not registered by default) | No |
Extension Architecture
Registration (lib/extensions/registry.ts): Singleton registry. register() wires event handlers to the bus. get(id), getAll(), getByCapability(key).
Context (lib/extensions/context-factory.ts): Every handler receives ExtensionContext with: userId, companyId, extensionId, supabase, emit(), settings (key-value in extension_data table), storage (Supabase Storage), log (prefixed logger), services (e.g., ingestTransactions).
API routes: Dispatched via catch-all at app/api/extensions/ext/[...path]/route.ts. URL: /api/extensions/ext/{extensionId}/{routePath}. Path params extracted as _paramName search params.
Service provider patterns:
- Interface registration (email): Core defines noop default in
lib/email/service.ts, extension callsregisterEmailService(), core usesgetEmailService(). - Services record (ai-categorization): Extension exposes via
servicesproperty, core looks up viaextensionRegistry.get('id')?.services?.method(...).
Creating extensions: npx tsx scripts/create-extension.ts --name my-ext --sector general --category operations --description "...", then add to extensions.config.json.
MCP Server & API Keys
gnubok exposes its bookkeeping engine as an MCP (Model Context Protocol) server, letting users do bookkeeping through Claude Desktop, Claude Code, or any MCP-compatible client.
MCP extension (extensions/general/mcp-server/): 35 tools — transactions, categorization, customers, suppliers, invoices, supplier invoices, accounts, fiscal periods, trial balance, general ledger, balance sheet, income statement, AR/supplier ledger, reconciliation, VAT report, KPI report, receipt matching, invoice payments/sending, inbox items, employees + salary runs (list/get/create/calculate), salary journal, AGI generation, document upload. JSON-RPC 2.0 protocol implemented directly (no SDK dependency). Endpoint: /api/extensions/ext/mcp-server/mcp.
API key infrastructure (lib/auth/api-keys.ts, api_keys table): SHA-256 hashed keys with gnubok_sk_ prefix. Scoped permissions mapped via TOOL_SCOPE_MAP. Rate limited at 100 RPM via atomic DB RPC (validate_and_increment_api_key). createServiceClientNoCookies() creates a Supabase service client without cookies for API key auth — all queries filter by company_id (defense in depth).
OAuth 2.1 for Claude Desktop connectors:
.well-known/oauth-protected-resourceand.well-known/oauth-authorization-server— discovery endpoints (excluded from auth middleware)/api/mcp-oauth/authorize— consent page + auth code generation/api/mcp-oauth/token— PKCE verification + API key creation/api/mcp-oauth/register— dynamic client registration- Stateless encrypted auth codes (AES-256-GCM via
lib/auth/oauth-codes.ts) - Single-use enforcement via
oauth_used_codestable - Redirect URI allowlist:
claude.ai/api/*,claude.com/api/*,localhost
npm package (packages/gnubok-mcp): Published as gnubok-mcp on npm. Stdio-to-HTTP bridge for Claude Desktop. Users configure npx gnubok-mcp with their API key.
API Route Pattern
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { validateBody } from '@/lib/api/validate'
import { MySchema } from '@/lib/api/schemas'
ensureInitialized() // Module-level — loads extensions for event emission
export async function POST(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const result = await validateBody(request, MySchema)
if (!result.success) return result.response
// Business logic... always filter by company_id (defense in depth alongside RLS)
return NextResponse.json({ data: result })
}
- Dynamic route params:
{ params }: { params: Promise<{ id: string }> }(Next.js 16) - Response shapes:
{ data }for success,{ error }for failures - Zod schemas in
lib/api/schemas.ts— 30+ schemas with shared primitives (uuid, isoDate, accountNumber, nonNegativeAmount)
Key lib/ Directories
| Directory | Purpose |
|---|---|
bookkeeping/ |
Engine, entry generators, mapping, templates, BAS data, template library + embeddings |
core/ |
Period service, year-end, storno, tax codes, audit, documents |
events/ |
Event bus singleton, 36 event types, event log handler |
auth/ |
API keys, require-auth, require-write, MFA, OAuth codes, invite tokens, cron auth, BankID |
supabase/ |
Browser/server/service clients, middleware, fetch-all pagination |
api/ |
Zod validation (validateBody/validateQuery), schemas |
reports/ |
20 report generators (financial statements, ledgers, tax, exports, salary journal, vacation liability, avgifter basis) |
invoices/ |
Invoice/supplier matching, payment match log, reminders, VAT rules, PDF template |
transactions/ |
Multi-source ingestion (ingest.ts), AI category suggestions |
import/ |
SIE parser/import, bank file import, opening balance, account mapper |
documents/ |
Document matcher, receipt matcher, batch matching |
extensions/ |
Registry, loader, context factory, types, generated files |
email/ |
Service interface (noop default), Resend provider, templates (invite, invoice, reminder, consent) |
company/ |
Company context resolution, CRUD actions, fiscal period computation |
company-lookup/ |
Shared types for org-number → company info lookups |
providers/ |
Third-party accounting provider adapters (Fortnox, Bokio, Briox, BL/Björn Lundén, Visma) with OAuth, rate limiting, retry, consent resolution |
salary/ |
Payroll calculation engine, tax tables, absence/benefits/traktamente, AGI, KU, PDF payslips, löneväxling, personnummer, payment, salary entries, transaction matcher |
processing-history/ |
Processing-history append helper for audit/inbox timelines |
reconciliation/ |
Bank statement reconciliation |
tax/ |
Tax calculator, deadline config/generator, expense warnings, Swedish holidays |
vat/ |
VIES client, EU countries, MOMS box mapping |
deadlines/ |
Deadline status engine |
currency/ |
Riksbanken exchange rates |
skatteverket/ |
Tax authority data formatting |
bankgiro/ |
Luhn checksum validation |
calendar/ |
ICS generator, calendar utilities |
errors/ |
Swedish error message mapping (Zod → Postgres → HTTP → fallback) |
hooks/ |
React hooks (e.g., use-unsaved-changes, use-can-write) |
logger.ts |
Structured logger with module prefixes, env-aware filtering |
support.ts |
Server-side support recipient email (used by /api/support/contact) |
utils.ts |
cn(), formatCurrency(), formatDate(), formatOrgNumber() |
App Routes
Pages
| Route | Purpose |
|---|---|
/login, /register, /reset-password |
Auth pages |
/mfa/enroll, /mfa/verify |
MFA flow |
/onboarding |
Multi-step company setup wizard |
/companies/new |
Create new company |
/invite/[token] |
Accept team/company invite |
/ |
Dashboard home |
/transactions |
Bank transaction list & categorization |
/invoices, /invoices/new, /invoices/[id], /invoices/[id]/credit |
Customer invoicing |
/supplier-invoices, /supplier-invoices/new, /supplier-invoices/[id] |
Supplier invoices |
/customers, /customers/[id] |
Customer management |
/suppliers, /suppliers/[id] |
Supplier management |
/expenses, /expenses/new, /expenses/[id] |
Expense tracking |
/receipts, /receipts/scan |
Receipt management |
/bookkeeping, /bookkeeping/[id], /bookkeeping/year-end |
Journal entries, chart of accounts, year-end |
/salary, /salary/employees, /salary/runs |
Payroll: employees, salary runs, AGI, KU |
/reports |
Financial reports |
/import |
SIE and bank file import |
/kpi |
KPI metrics + monthly trend chart |
/deadlines |
Tax & business deadlines |
/pending |
Pending operations queue |
/help |
In-app help/support page |
/extensions, /extensions/[sector]/[extension] |
Extension marketplace |
/e/[sector]/[slug] |
Extension workspace |
/settings/* |
Company, invoicing, bookkeeping, tax, team, banking, templates, salary, backup, account, API settings |
/dpa, /privacy |
Legal pages |
/invoice-action/[token] |
Public invoice payment link |
/sandbox |
Test environment |
API Endpoints (key groups)
/api/bookkeeping/*— Accounts, fiscal periods (close/lock/year-end/opening-balances/currency-revaluation), journal entries (CRUD/reverse/correct/chain), mapping rules, voucher gaps/api/invoices/*— CRUD, send, mark-sent/paid, convert, PDF, reminders cron/api/supplier-invoices/*— CRUD, approve, mark-paid, credit/api/transactions/*— Categorize, uncategorize, describe, book, match-invoice, match-supplier-invoice, batch operations, AI suggestions/api/customers/*,/api/suppliers/*— CRUD/api/documents/*— CRUD, versions, link, verify, match-sweep, verify cron/api/reports/*— 19 report endpoints (general-ledger, trial-balance, balance-sheet, income-statement, journal-register, ar-ledger, supplier-ledger, vat-declaration, sie-export, ink2, ne-bilaga, kpi, audit-trail, continuity-check, monthly-breakdown, full-archive, salary-journal, vacation-liability, avgifter-basis)/api/salary/*— Employees, payroll-config, tax-tables, KU, salary runs (CRUD + calculate)/api/import/*— Bank file (parse/execute), SIE (parse/execute/mappings/create-accounts)/api/reconciliation/bank/*— Link, unlink, run, status, unmatched-entries/api/settings/*— Company settings, API keys, logo upload, counterparty templates, booking templates/api/company/*— Current, members (list/CRUD/invite),[id]/api/team/*— Accept, invite, members/api/deadlines/*,/api/tax-deadlines/*— Deadline CRUD and crons/api/pending-operations/*— Queue, commit, reject/api/events/*— Event log and cleanup cron/api/documents/*— CRUD, counts, match-sweep, verify cron/api/calendar/feed/[token]— iCal subscription feed/api/mcp-oauth/*— Register, authorize, token/api/support/contact— Support contact form submission/api/account/delete— User account deletion/api/audit-trail/*— Audit trail queries/api/log— Client-side log ingestion/api/health— Health check/api/vat/validate— VIES VAT validation/api/currency/rate— Riksbanken exchange rate lookup/api/sandbox/*— Seed, cleanup cron/api/extensions/ext/[...path]— Dynamic extension API routes (plus top-level/api/extensions/enable-banking/*,/api/extensions/cloud-backup/*,/api/extensions/push-notifications/*)
Testing
Framework: Vitest 4, globals: true, environment: 'node'. Tests colocated in __tests__/ directories. Scope: business logic in lib/ and API routes in app/api/. No component or E2E tests.
Test helpers (tests/helpers.ts): createMockSupabase() (chainable proxy), createQueuedMockSupabase() (sequential calls), createMockRequest(), parseJsonResponse(), createMockRouteParams(), and fixture factories: makeTransaction(), makeJournalEntry(), makeJournalEntryLine(), makeInvoice(), makeInvoicePayment(), makeCustomer(), makeSupplier(), makeSupplierInvoice(), makeFiscalPeriod(), makeReceipt(), makeDocumentAttachment(), makeCompanySettings(), makeCompany(), makeCompanyMember(), makeInvoiceInboxItem(), makeTaxCode(), makeCategorizationTemplate(), makeSIEVoucher(), makeBankConnection().
Patterns: Always mock @/lib/supabase/server. Use vi.clearAllMocks() and eventBus.clear() in beforeEach. API route tests: mock @/lib/init and lib functions, test auth (401), validation (400), not found (404), errors (500), happy path.
Database & Migrations
Location: supabase/migrations/ — 118 files. Early migrations use sequential numbering (20240101000001–20240101000038), later ones use real timestamps.
Key Tables (~60)
Multi-tenant: companies, company_members, company_invitations, teams, team_members, team_invitations, user_preferences, profiles
Bookkeeping: chart_of_accounts, fiscal_periods, journal_entries, journal_entry_lines, account_balances, voucher_sequences, voucher_gap_explanations
Invoicing: customers, invoices, invoice_items, invoice_payments, invoice_inbox_items
Suppliers: suppliers, supplier_invoices, supplier_invoice_items
Banking: bank_connections, transactions, bank_file_imports, payment_match_log
Documents: document_attachments (WORM), receipts, receipt_line_items
Settings & Config: company_settings, mapping_rules, categorization_templates, booking_template_library, extension_data
Dimensions: cost_centers, projects
Tax & Deadlines: tax_rates, tax_table_rates, deadlines, calendar_feeds, skatteverket_tokens
API & Auth: api_keys (with scopes), oauth_used_codes, bankid_identities
Audit & Ops: audit_log (immutable), event_log (30-day TTL), pending_operations, processing_history (+ processing_event_types), ai_usage_tracking, voucher_gap_explanations, automation_webhooks
Inbox & Migration: invoice_inbox_items, company_inboxes, email_connections
Salary: employees, salary_runs, salary_run_employees, salary_line_items, salary_payroll_config, agi_declarations
Third-party providers: provider_consents, provider_consent_tokens, provider_otc
Other: sandbox_users
Key RPC Functions
create_company_with_owner()— Atomic company + owner creationcommit_journal_entry()— Atomic draft→posted with voucher numbernext_voucher_number()— Concurrent-safe voucher generationdetect_voucher_gaps()— BFNAR 2013:2 gap detectiongenerate_invoice_number(),get_next_arrival_number(),generate_delivery_note_number()— Sequence generatorsseed_chart_of_accounts()— BAS chart seeding per entity typevalidate_and_increment_api_key()— Atomic rate limitinguser_company_ids()— RLS helper returning user's company IDsget_unlinked_1930_lines()— Bank reconciliation helpercleanup_sandbox_user(),cleanup_expired_sandbox_users()— Sandbox lifecycle
Key Triggers
check_journal_entry_balance()— Debit must equal creditenforce_journal_entry_immutability()— Posted entries cannot be modifiedenforce_period_lock()— No entries in closed/locked periodsenforce_company_lock_date()— Company-wide bookkeeping lock dateblock_document_deletion()— WORM complianceenforce_retention_journal_entries()— 7-year retentionaudit_log_immutable()— Audit log cannot be modifiedwrite_audit_log()— Auto-audit on DML operationssync_team_member_to_companies()— Auto-sync team→company membership
Migration Rules
- Always enable RLS and create policies using
user_company_ids()for company-scoped data - Always add
updated_attrigger usingupdate_updated_at_column() - UUID primary keys:
DEFAULT uuid_generate_v4() - Company ownership:
company_id UUID REFERENCES companies NOT NULL+user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL - Never modify existing migrations — create new ones
- Never modify enforcement triggers (migration 017) — legally required
- Apply via Supabase MCP tool:
mcp__plugin_supabase_supabase__apply_migration - Always include
NOTIFY pgrst, 'reload schema'at the end of migrations that alter table structure (ADD/DROP COLUMN, CREATE TABLE, ALTER TYPE). Without this, PostgREST serves stale schema until next reload.
Skills, Git & CI
Skills: Always use /frontend-design for new UI. Use vercel:deploy for deployment. Use /supabase-migration for new migrations. Use /erp-api-route for new API routes. Use /create-extension for new extensions. Use the Swedish domain skills (swedish-sie-import-export, swedish-accounting-compliance, swedish-vat, swedish-invoice-compliance, swedish-payroll, swedish-year-end-closing, swedish-financial-reporting, swedish-sru-filing, swedish-asset-accounting, swedish-project-accounting, swedish-tax-planning) for accounting domain questions.
Git: Conventional commits (feat:, fix:, refactor:, test:, docs:). Atomic commits, branch from main.
CI:
.github/workflows/core-build.yml— resets extensions to empty, runs build + test, verifies no core code imports from@/extensions/directly..github/workflows/swedish-compliance-review.yml— Swedish accounting compliance review on PRs touching bookkeeping/reports/tax logic..github/workflows/docker-publish.yml— pushes images to GHCR on main.
Docker (.github/workflows/docker-publish.yml): Pushes to GHCR (erp-mafia/erp-base) on main push. 4-stage Dockerfile (base → deps → builder → runner) with Node 22 Alpine. Runtime env placeholder replacement via docker-entrypoint.sh. Docker Compose with app + supercronic cron service.
Deployment
Vercel (Hosted)
Cron jobs defined in vercel.json:
| Schedule | Endpoint | Purpose |
|---|---|---|
0 6 * * * |
/api/deadlines/status/cron |
Update deadline statuses |
0 8 * * * |
/api/invoices/reminders/cron |
Send invoice reminders |
0 0 2 1 * |
/api/tax-deadlines/cron |
Generate tax deadlines |
0 5 * * * |
/api/extensions/enable-banking/sync/cron |
Bank transaction sync |
0 3 * * * |
/api/documents/verify/cron |
Document integrity verification (daily) |
0 4 * * * |
/api/sandbox/cleanup/cron |
Sandbox user cleanup |
0 2 * * * |
/api/events/cleanup/cron |
Event log cleanup (30-day TTL) |
0 * * * * |
/api/extensions/cloud-backup/auto-sync/cron |
Hourly cloud backup auto-sync |
Docker (Self-Hosted)
Dockerfile: 4-stage Node 22 Alpine build with standalone outputdocker-compose.yml: App service + supercronic cron schedulerdocker-entrypoint.sh: Validates required env vars, replaces build-time placeholders in.next/static/JS- Extension presets:
docker/extensions.self-hosted.json,docker/extensions.hosted.json
Environment Variables
Required: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_APP_URL, CRON_SECRET
Auth: NEXT_PUBLIC_REQUIRE_MFA (set true on hosted), NEXT_PUBLIC_SELF_HOSTED (set true for Docker)
Extension-specific (only when extension is enabled): ENABLE_BANKING_APP_ID/ENABLE_BANKING_APP_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, RESEND_API_KEY, VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY
Optional: SENTRY_DSN, SENTRY_AUTH_TOKEN
Other
Never create a NUL/nul file: \gnubok\NUL
Design Context
Users
Swedish sole traders (enskild firma) and small business owners (aktiebolag) who need to manage their own bookkeeping. They are not accountants — they are professionals (consultants, freelancers, shop owners) who want to stay compliant without hiring one. They use gnubok in short, focused sessions: sending an invoice, categorizing bank transactions, filing a VAT declaration. Speed and clarity matter — every second spent in the app is a second away from their real work.
Brand & Aesthetic
Minimal. Sharp. Efficient. The interface should feel like a well-made instrument: considered, quiet, and confident. Reference: Mercury (banking). Anti-reference: enterprise software (SAP/Oracle density).
- Palette: Grayscale foundation with restrained semantic colors — sage green (success/balance), terracotta (errors/overdue), ochre (warnings/attention). No loud brand color.
- Typography: Fraunces (serif) for display headings, Geist (sans) for body. Tabular numbers everywhere financial data appears.
- Surfaces: White/near-white cards on light gray backgrounds. Subtle borders (60% opacity). Soft shadows. Dark mode follows the same restraint.
- Spacing: Generous whitespace. Dense data (tables, ledgers) uses tighter spacing but never feels cramped.
- Motion: Subtle and purposeful. Stagger animations for list entry, spring easing for feedback. Never decorative.
- Icons: Lucide — 15px in navigation, slightly larger in empty states.
Design Principles
- Clarity over cleverness. Every element immediately understandable. Clear labels (in Swedish), obvious hierarchy.
- Earned minimalism. Remove what doesn't serve the task, but don't strip context that prevents compliance errors.
- Numbers are first-class. Tabular-nums, proper alignment, adequate contrast, clear positive/negative distinction.
- Trust through consistency. Same patterns, spacing, and behavior everywhere.
- Speed is a feature. Optimize for the 90-second session.
Accessibility
- WCAG AA: 4.5:1 text contrast, 3:1 UI components
- Keyboard-navigable with visible focus rings
- Respect
prefers-reduced-motion - Color never sole indicator of state — always pair with icons, text, or shape