* feat: delete last voucher, notes field, schema cache fix
Address three customer feedback items from William (wigu.se):
1. Delete last voucher per series (Fortnox model):
- New `delete_last_voucher` RPC with full safety checks (last-in-series,
open period, no references, owner/admin only)
- Session variable bypass for immutability/retention/line triggers
- Full JSONB audit trail (BFNAR 2013:2 behandlingshistorik)
- DELETE endpoint + UI with confirmation dialogs
- Storno restoration when deleting a reversal entry
2. Notes/comment field on vouchers:
- `notes` column on journal_entries (always-editable internal metadata)
- Immutability trigger updated to allow notes-only updates on posted entries
- PATCH endpoint, inline-edit UI on detail page, form textarea
3. Schema cache fix:
- NOTIFY pgrst applied to production (immediate fix)
- Retroactive migration + CLAUDE.md migration rule added
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Greptile review — tighten trigger, lock voucher sequence
P1: The notes-only exception in enforce_journal_entry_immutability was
too broad — it only checked 7 verifikation fields, allowing silent
mutation of correction_of_id, reverses_id, reversed_by_id, committed_at,
and user_id on posted entries. Now guards all metadata fields; only
notes and updated_at may differ.
P2: Lock voucher_sequences row FOR UPDATE before the MAX(voucher_number)
check in delete_last_voucher to serialise against concurrent
commit_journal_entry calls, preventing voucher number gaps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
30 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, OpenAI (embeddings), Resend (email), Sentry (error tracking), Svix (webhooks), web-push (notifications), JSZip (archive export), sharp (image processing), Framer Motion (animations).
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. 30+ 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. - Core reports (in
lib/reports/, not extensions): balance sheet, income statement, trial balance, general ledger, AR/supplier ledger, AR/supplier reconciliation, bank reconciliation status, VAT declaration, journal register, monthly breakdown, continuity check, opening balances, KPI, NE-bilaga, INK2 declaration, SIE export, full archive export. - Types: All shared types in
types/index.ts(~2,200 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 |
receipt-ocr |
AI receipt scanning and extraction | No |
ai-categorization |
AI transaction categorization | No |
ai-chat |
AI assistant for bookkeeping questions | No |
push-notifications |
Web push notifications for events | No |
invoice-inbox |
Email-based invoice document processing | No |
calendar |
Payment calendar with iCal feed | No |
skatteverket |
Skatteverket VAT declaration submission | 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/): 26 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. 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 |
core/ |
Period service, year-end, storno, tax codes, audit, documents |
events/ |
Event bus singleton, 30+ event types, event log handler |
auth/ |
API keys, require-auth, MFA, OAuth codes, invite tokens, cron auth |
supabase/ |
Browser/server/service clients, middleware, fetch-all pagination |
api/ |
Zod validation (validateBody/validateQuery), schemas |
reports/ |
17 report generators (financial statements, ledgers, tax, exports) |
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, 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 |
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) |
settings/ |
Settings utilities |
logger.ts |
Structured logger with module prefixes, env-aware filtering |
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 |
/reports |
Financial reports |
/import |
SIE and bank file import |
/kpi |
KPI metrics + monthly trend chart |
/deadlines |
Tax & business deadlines |
/pending |
Pending operations queue |
/extensions, /extensions/[sector]/[extension] |
Extension marketplace |
/e/[sector]/[slug] |
Extension workspace |
/settings/* |
Company, invoicing, bookkeeping, tax, team, banking, templates, 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/*— 16 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)/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/api/company/members/*— List, CRUD, invite/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/calendar/feed/[token]— iCal subscription feed/api/mcp-oauth/*— Register, authorize, token/api/health— Health check/api/vat/validate— VIES VAT validation/api/sandbox/*— Seed, cleanup cron/api/extensions/ext/[...path]— Dynamic extension API routes
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/ — 93 files. Early migrations use sequential numbering (20240101000001–20240101000038), later ones use real timestamps.
Key Tables (~47)
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, extension_data
Dimensions: cost_centers, projects
Tax & Deadlines: tax_rates, deadlines, calendar_feeds, skatteverket_tokens
API & Auth: api_keys (with scopes), oauth_used_codes
Audit & Ops: audit_log (immutable), event_log (30-day TTL), pending_operations, ai_usage_tracking
Other: salary_payments, 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 /swedish-bookkeeping 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.
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 * * 0 |
/api/documents/verify/cron |
Document integrity verification |
0 4 * * * |
/api/sandbox/cleanup/cron |
Sandbox user cleanup |
0 2 * * * |
/api/events/cleanup/cron |
Event log cleanup (30-day TTL) |
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