Files
accounted/CLAUDE.md
T
Jakob WennbergandClaude Opus 4.6 a7be1aab17 feat: comprehensive UI design audit and normalization (#28)
* feat: import system improvements, INK2 fix, and Swedish text corrections

- SIE parser: Windows-1252 and CP437 encoding detection and decoding
- Bank file parser: add Nordea Business (Företag) CSV format
- Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV
- INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years
- Dashboard: parallel Supabase queries, simplified dashboard page
- Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures
- Import wizard UI improvements across all steps
- Migration: add 'bas_range' match type to sie_account_mappings constraint
- Extensive new tests for SIE parser encoding and bank file parser

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: arcim migration wizard UX fixes, Sentry setup, and extension scaffolding

Arcim migration wizard improvements:
- Progress bar now excludes non-interactive steps (migrating/result)
- Fix OAuth text to match target="_blank" behavior (new tab, not redirect)
- Display month names instead of "Månad X" in preview
- Fix Swedish typo "förifylla" in no-company-info message
- Replace native checkboxes with shadcn Switch in options step
- Add ConfirmationDialog before starting migration
- Show progress percentage during migration
- Add "Nästa steg" guidance and navigation links in result step
- Add "Försök igen" button in error state (returns to options)
- Add Bokio company ID help text (GUID from URL)
- Add Fortnox integration add-on hint on connection failure

Also includes: SIE import system improvements, INK2 fixes, Swedish text
corrections, Sentry error tracking setup, and arcim-migration extension
scaffolding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback

- Fix OAuth error recovery blank page (restore provider from URL params)
- Pass real userId to MigrationWizard instead of empty string
- Remove ~50 debug console.log statements from sie-import.ts
- Fix comment referencing account 3740 → 3741

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: comprehensive UI design audit and normalization

Dashboard audit:
- Fix muted-foreground contrast (4.31:1 → 5.08:1) for WCAG AA
- Add prefers-reduced-motion media query for all animations
- Replace border-l-2 accent anti-pattern with subtle full-border colors
- Add aria-expanded to toggle buttons, role="status" to live counters
- Fix touch targets on deadline buttons (28px → 36px)
- Vary section spacing for rhythm (mb-12/mb-10/mb-8)
- Remove unused imports and dead code

Transactions audit + hardening:
- Add pagination (200 per page) with "Ladda fler" button
- Replace height animation with transform-only exit animation
- Show batch progress in floating action bar during processing
- Fix batch bar mobile overlap (bottom-20 on mobile)
- Replace clickable badges with proper button elements
- Add safe area padding to fullscreen swipe view
- Add response.ok check to suggestion fetch
- Add truncation to invoice number buttons

Invoicing audit:
- Remove border-l-4 accent pattern from invoice cards
- Replace string concatenation with cn() utility

Systemic sweep (34 files):
- All page headings: font-bold → font-display font-medium (Fraunces)
- All stat numbers: font-bold → font-display font-medium tabular-nums
- All hard-coded blue/amber/emerald colors → design tokens
- Remove all dark mode overrides (tokens handle automatically)
- Tint pure white card background to 99%

Design context added to CLAUDE.md with brand personality,
aesthetic direction, and 5 design principles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bookkeeping flow audit — design system, accessibility, UX

- Replace raw <select> with shadcn Select component (JournalEntryForm)
- Add confirmation dialog for account deletion (ChartOfAccountsManager)
- Remove console.error from production code (JournalEntryList, JournalEntryForm)
- Fix contradictory h-7/min-h-[44px] button sizing → h-10 (ChartOfAccountsManager)
- Increase BAS catalog "Lägg till" touch target h-7 → h-9
- Improve loading state with spinner (JournalEntryList)
- Improve empty state with icon, description, and guidance (JournalEntryList)
- Add response.ok check on journal entry fetch
- Add aria-expanded to entry expand buttons
- Add tabular-nums to desktop debit/credit columns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: onboarding and empty state improvements

Onboarding:
- Replace font-serif with font-display (Fraunces) for brand consistency
- Remove console.error calls from production code

Empty states:
- Fix broken /transactions/new link in EmptyTransactions (route doesn't exist)
- Add actionHref fallback to EmptyCustomers when no onAction prop provided
- Improve EmptyTransactions description copy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clarify Swedish UX copy — terminology, errors, descriptions

Terminology consistency:
- "Försenad" → "Förfallen" for overdue invoices (customers/[id])
- "bokföringsorder" → actionable description in bookkeeping page
- "verifikation har bifogats" → "underlag har bifogats" in doc warning
- "Fortsätt ändå" → "Bokför utan underlag" (specific action)

Error messages — replace generic "Fel" + "Något gick fel" with specific:
- "Något gick fel vid bokföring" → "Transaktionen kunde inte bokföras"
- "Något gick fel vid matchning" → "Transaktionen kunde inte matchas"
- "Kunde inte hämta X" → "Kunde inte ladda X" + recovery hint
- Add "Försök igen" guidance to all error toasts

Page descriptions — replace redundant with actionable:
- Invoices: "Skapa och hantera" → "Skicka, följ betalningar, skapa kreditnotor"
- Bookkeeping: list of features → actionable description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: design critique — dashboard affordance, reports description

Dashboard:
- Add ChevronRight indicator to clickable summary cards
  (Att få betalt, Koppla bank) to distinguish from static cards
- Add cursor-pointer to linked cards

Reports:
- Replace feature list description with actionable guidance
  "Huvudbok, grundbok..." → "Generera skattedeklarationer..."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace generic "Fel" error toasts with specific messages

Deadlines: 5 generic "Fel" → specific per-action titles
  (create, toggle, edit, delete, load)
Expenses detail: 5 generic "Fel" → specific per-action titles
  (load, approve, pay, credit, delete)
Expenses new: 3 generic "Fel" → instructional validation messages
  (supplier name, supplier selection, invoice number)
Customers: 1 generic "Fel" → specific load error with recovery hint

All error toasts now follow pattern:
  title = what failed, description = how to recover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace all remaining generic "Fel" error toasts (37 instances)

Systematic sweep across 12 dashboard pages replacing generic
title: 'Fel' with context-specific error titles:

- Load errors: "Kunde inte ladda [resurs]"
- Action errors: "[Åtgärd] misslyckades"
- Validation: "[Fält] saknas"

Every error toast now tells the user what failed without needing
to read the description. Recovery hints added where missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: import flow — normalize stat typography, remove console.warn

- Replace font-bold with font-display font-medium on 13 stat numbers
  across SIEPreviewStep, BankFilePreviewStep, BankFileConfirmStep,
  ImportResultStep (missed by systemic sweep since these are in
  components/import/, not app/(dashboard)/)
- Add tabular-nums to stat numbers displaying counts/currency
- Remove console.warn in ArcimMigrationWorkspace

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: final cleanup — console statements, remaining font-bold stats

Remove production console statements:
- Step1EntityType: remove debug console.warn (dead code after onNext)
- TransactionBookingDialog: remove console.error on doc link failure
- JournalEntryAttachments: remove 3 console.error calls

Normalize remaining font-bold stat displays:
- SwipeCategorizationView: 3 instances (completion, amount displays)
- NEDeclarationView: yearly result heading + value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Greptile review feedback

loadMoreTransactions: add inbox item enrichment matching fetchTransactions
- Paginated transactions now fetch invoice_inbox_items in parallel
- Fixes missing document indicator, template suggestions, and inbox
  match card for transactions loaded via "Ladda fler"

fetchAllPages: add maxPages guard (default 500) to prevent infinite loop
- If Arcim gateway returns hasMore:true indefinitely, the loop now
  exits after 500 pages instead of running forever

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:48:40 +01:00

24 KiB
Raw Blame History

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.

Tech stack: Next.js 16 (App Router), React 19, TypeScript (strict), Supabase (PostgreSQL + RLS + email/password + TOTP MFA auth), Tailwind CSS 4 + shadcn/ui, Vercel hosting.

Integrations: Enable Banking (PSD2), Anthropic SDK, LangChain, OpenAI (embeddings), Resend (email), JSZip (archive export).

Path alias: @/* maps to the project root. Language: All code, comments, and commit messages in English.


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

Architecture

app/
  (auth)/              Login, register, auth callback, MFA enroll/verify, password reset
  (onboarding)/        6-step setup wizard
  (dashboard)/         Authenticated routes (invoices, customers, transactions,
                       bookkeeping, reports, suppliers, supplier-invoices,
                       receipts, deadlines, settings, help, import, extensions)
  (public)/            Public invoice action links (no auth), DPA, privacy policy
  api/                 API routes organized by domain

components/
  ui/                  shadcn/ui primitives
  bookkeeping/         Chart of accounts, journal entry form/list/review
  chat/                ChatWidget, ChatPanel
  extensions/          Extension marketplace UI, general/ workspace components
  transactions/        Transaction list, categorization, booking, VAT treatment
  (+ customers/, dashboard/, deadlines/, import/, invoices/,
     onboarding/, reports/, settings/, suppliers/)

extensions/general/    Config-driven extensions (see Extension System below)

lib/
  api/                 Zod validation schemas (schemas.ts) and helpers (validate.ts)
  bookkeeping/         Core journal entry engine and all entry generators
    engine.ts          Draft/commit workflow, balance validation, voucher numbering
    invoice-entries.ts Sales invoice journal entries (per-line VAT rates)
    transaction-entries.ts  Bank transaction journal entries
    supplier-invoice-entries.ts  Purchase invoice journal entries
    category-mapping.ts     Category-to-BAS-account mapping
    mapping-engine.ts       Rule-based auto-categorization (MCC, merchant patterns)
    bas-reference.ts        BAS account catalog (~180 accounts)
    handlers/               Booking handler functions
  core/
    bookkeeping/       Period service, storno reversal, year-end closing
    documents/         Document archive (upload, versioning, SHA-256 integrity)
    audit/             Audit trail service
    tax/               Tax code service
  email/               EmailService interface + NoopEmailService default
  events/              Event bus (bus.ts, types.ts) — singleton, core emits, extensions subscribe
  extensions/          Extension system (loader, registry, types, hooks, context-factory)
    ai-consent.ts      AI consent gate for extensions using third-party AI providers
    _generated/        Code-generated files (DO NOT EDIT)
  import/              SIE parser, bank file parser (10 Swedish bank formats)
  invoices/            VAT rules, invoice matching, PDF template, reminders
  reconciliation/      Bank reconciliation engine (4-pass matching)
  reports/             Financial reports (trial balance, income statement, balance sheet,
                       VAT declaration, SIE export, general ledger, NE-bilaga, INK2, SRU export,
                       full archive ZIP export)
  auth/                MFA helpers (mfa.ts) and API auth guard (require-auth.ts)
  supabase/            Client setup (client.ts = browser, server.ts = server)
  tax/                 Tax calculations, deadlines, Swedish holidays
  vat/                 VIES validation, moms box mapping
  init.ts              Extension loader (idempotent, called by API routes)

types/index.ts         Canonical type definitions (single source of truth)
types/chat.ts          Chat types
tests/helpers.ts       Mock factories and fixture builders
supabase/migrations/   SQL migration files (63 files)
extensions.config.json Extension opt-in configuration

Key Relationships

  • All journal entry creation routes through lib/bookkeeping/engine.ts via createJournalEntry().
  • API routes that emit events must call ensureInitialized() (from lib/init.ts) at module level.
  • Event bus (lib/events/bus.ts) is a module-level singleton. Handlers run via Promise.allSettled.
  • Supabase clients: browser (lib/supabase/client.ts), server with cookies (createClient() from server.ts), service role (createServiceClient()).
  • Extension system: Opt-in via extensions.config.json. Core builds and runs with zero extensions.
  • NE-bilaga, INK2 declaration, SRU export, and full archive export are core reports (in lib/reports/), not extensions.
  • AI consent gate (lib/extensions/ai-consent.ts): AI extensions (receipt-ocr, ai-categorization, ai-chat) require user consent before API calls. The extension catch-all route checks consent and returns 403 AI_CONSENT_REQUIRED if missing.

Authentication

Authentication uses Supabase Auth with email+password (primary) and magic link (fallback). MFA via TOTP is supported.

Auth Flow

  1. Login (/login) — Email+password with magic link toggle. After password login, checks MFA status.
  2. Register (/register) — Email+password signup. Supabase sends confirmation email. Strong password required (8+ chars, uppercase, lowercase, number, special character).
  3. Password reset (/reset-password) — Via recovery link from login page.
  4. MFA verify (/mfa/verify) — 6-digit TOTP code input after login when MFA is enrolled.
  5. MFA enroll (/mfa/enroll) — QR code + manual secret for authenticator app setup.

MFA Enforcement

MFA is enforced application-side (middleware + API routes), not in RLS policies. This is controlled by two env vars:

Env Var Hosted (Vercel) Self-hosted (Docker)
NEXT_PUBLIC_REQUIRE_MFA true false (default)
NEXT_PUBLIC_SELF_HOSTED not set true
  • Self-hosted: MFA is never enforced (NEXT_PUBLIC_SELF_HOSTED=true overrides). Users can still enable it voluntarily via Settings → Säkerhet.
  • Hosted: When NEXT_PUBLIC_REQUIRE_MFA=true, middleware redirects users without MFA to /mfa/enroll after onboarding, and users with MFA to /mfa/verify until AAL2 is achieved.

Key Files

File Purpose
lib/auth/mfa.ts isMfaRequired() — checks env vars
lib/auth/require-auth.ts requireAuth() — API route guard (auth + MFA)
lib/supabase/middleware.ts Session refresh + MFA enforcement
app/(auth)/auth/callback/route.ts PKCE/magic link callback with MFA redirect
components/settings/SecuritySettings.tsx Password change + MFA enable/disable UI

Supabase MFA Setup

Enable TOTP in Supabase Dashboard → Authentication → Multi-Factor Authentication. No database migration needed — Supabase manages auth.mfa_factors, auth.mfa_challenges, and auth.mfa_amr_claims internally.


Core Bookkeeping Engine

The engine (lib/bookkeeping/engine.ts) is the most critical system. All accounting flows route through it.

Journal Entry Lifecycle

  1. createDraftEntry(userId, input) — Creates status: 'draft', voucher_number: 0. Validates balance. Emits journal_entry.drafted.
  2. commitEntry(userId, entryId) — Assigns voucher number via DB RPC (concurrent-safe). Sets status: 'posted'. Emits journal_entry.committed.
  3. createJournalEntry(userId, input) — Draft + commit in one call. This is what all entry generators use.
  4. reverseEntry(userId, entryId) — Storno reversal: swaps debit/credit, links via reverses_id/reversed_by_id.

Entry Generators

Function File Purpose
createInvoiceJournalEntry() invoice-entries.ts Debit 1510, Credit 30xx + 26xx VAT (per-line VAT rates)
createInvoicePaymentJournalEntry() invoice-entries.ts Debit 1930, Credit 1510
createCreditNoteJournalEntry() invoice-entries.ts Reverses original invoice entry
createTransactionJournalEntry() transaction-entries.ts Maps bank transactions via MappingResult
createSupplierInvoiceRegistrationEntry() supplier-invoice-entries.ts Debit expense + 2641, Credit 2440
createSupplierInvoicePaymentEntry() supplier-invoice-entries.ts Debit 2440, Credit 1930

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). generatePerRateLines() in invoice-entries.ts groups by rate. Use getAvailableVatRates(customerType, vatNumberValidated) from lib/invoices/vat-rules.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) - ruta 48

The VatDeclaration.breakdown.invoices also includes base25/base12/base6 for per-rate revenue breakdown in the UI.

Bank Reconciliation

lib/reconciliation/bank-reconciliation.ts — 4-pass matching on account 1930:

  1. auto_exact (0.95) — exact amount + exact date
  2. auto_reference (0.90) — exact amount + reference match
  3. auto_date_range (0.85) — exact amount + date ±3 days
  4. auto_fuzzy (0.75) — fuzzy amount (±0.01) + exact date

Accounting Guard Rails

These rules exist for legal compliance, enforced by database triggers. Never violate them.

  1. Committed entries are immutable. Once status: 'posted', cannot be edited or deleted (DB trigger).
  2. Never delete posted entries. Use reverseEntry() (storno) to cancel.
  3. Every entry must balance. sum(debits) === sum(credits), both > 0.
  4. Voucher numbers are sequential. Assigned via DB RPC. Never set manually.
  5. Period lock enforcement. DB trigger blocks writes to closed/locked periods.
  6. 7-year document retention. DB triggers prevent deletion of documents linked to posted entries.
  7. Storno, never edit. Use correctEntry() from lib/core/bookkeeping/storno-service.ts.
  8. Use Math.round(x * 100) / 100 for monetary calculations. Never toFixed().
  9. Always use engine functions. Never insert directly into journal tables.
  10. Account numbers are strings. '1930', never 1930.

Extension System

Extensions are opt-in plugins controlled by extensions.config.json. Core builds and runs with zero extensions.

How It Works

  1. Each extension has a manifest.json in extensions/general/<name>/.
  2. extensions.config.json lists enabled extension IDs.
  3. npm run setup:extensions generates files in lib/extensions/_generated/ (static imports, workspace map, definitions).
  4. predev/prebuild hooks run this automatically.

Available Extensions

Extension Category Env Vars Required
receipt-ocr import ANTHROPIC_API_KEY
ai-categorization operations ANTHROPIC_API_KEY, OPENAI_API_KEY
ai-chat operations ANTHROPIC_API_KEY, OPENAI_API_KEY
invoice-inbox import ANTHROPIC_API_KEY
calendar operations —
enable-banking import Enable Banking keys
email operations RESEND_API_KEY, RESEND_FROM_EMAIL

Creating Extensions

npx tsx scripts/create-extension.ts --name my-ext --sector general --category operations --description "..."

Then add "my-ext" to extensions.config.json and run npm run setup:extensions.

Constraints: Extensions cannot use dynamic imports (Next.js bundling).

Extension Interface

interface Extension {
  id: string; name: string; version: string; sector?: SectorSlug
  // All optional surfaces:
  apiRoutes?: ApiRouteDefinition[]
  eventHandlers?: ExtensionEventHandler[]
  services?: Record<string, (...args: any[]) => Promise<any>>
  sidebarItems?: SidebarItem[]
  mappingRuleTypes?: MappingRuleTypeDefinition[]
  settingsPanel?: SettingsPanelDefinition
  onInstall?(ctx: ExtensionContext): Promise<void>
  onUninstall?(ctx: ExtensionContext): Promise<void>
}

Extension Context

Handlers receive an ExtensionContext with: userId, extensionId, supabase (pre-authenticated), emit(), settings (JSONB key-value), storage (Supabase Storage), log (scoped logger), services.

Extension API Routes

Dispatched via catch-all at app/api/extensions/ext/[...path]/route.ts. URL scheme: /api/extensions/ext/{extensionId}/{routePath}

Path params extracted as _paramName search params (e.g., /:id → searchParams.get('_id')).

Service Provider Patterns

Interface registration (email pattern): Core defines interface with noop default in lib/email/service.ts. Extension calls registerEmailService() at load time. Core uses getEmailService() — degrades gracefully.

Services record (ai-categorization pattern): Extension exposes functions via services property. Core looks up via registry: extensionRegistry.get('ai-categorization')?.services?.findSimilarTemplates(...).

Event Types (lib/events/types.ts)

journal_entry.drafted/committed/corrected | document.uploaded | invoice.created/sent | credit_note.created | transaction.synced/categorized/reconciled | period.locked/year_closed | customer.created | receipt.extracted/matched/confirmed | supplier_invoice.received/extracted/confirmed


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() / createQueuedMockSupabase() — Supabase mocks
  • createMockRequest(), parseJsonResponse(), createMockRouteParams() — API route testing
  • Fixture factories: makeTransaction(), makeJournalEntry(), makeInvoice(), makeCustomer(), makeSupplier(), makeFiscalPeriod(), makeReceipt(), makeDocumentAttachment(), makeCompanySettings(), etc.

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/ — 63 files. Early migrations use sequential numbering (20240101000001–20240101000038), later ones use real timestamps (20260223150836+). Next migration: Use mcp__plugin_supabase_supabase__apply_migration which assigns timestamps automatically.

Placeholder Migrations

Some migrations are no-op placeholders to preserve the numbering sequence:

  • 012 (tax_codes_placeholder) — Planned but never deployed. The system operates without the tax_codes table.
  • 023 (document_version_chain_placeholder) — Planned but never deployed. Document versioning columns/functions do not exist in production.

Migration Rules

  1. Always enable RLS and create SELECT/INSERT/UPDATE policies using auth.uid() = user_id
  2. Always add updated_at trigger using update_updated_at_column()
  3. UUID primary keys: DEFAULT uuid_generate_v4()
  4. User ownership: user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL
  5. Never modify existing migrations — create new ones
  6. Never modify enforcement triggers (migration 017) — legally required
  7. Apply via Supabase MCP tool: mcp__plugin_supabase_supabase__apply_migration

Key Enforcement Triggers (migration 017)

  • enforce_journal_entry_immutability — Blocks edits/deletes on posted/reversed entries
  • enforce_journal_entry_line_immutability — Blocks line modifications on committed entries
  • enforce_period_lock — Blocks writes to closed/locked fiscal periods
  • block_document_deletion — Prevents deletion of documents linked to committed entries
  • enforce_retention_journal_entries — 7-year retention enforcement
  • set_committed_at — Auto-sets timestamp on draft-to-posted transition
  • calculate_retention_expiry — Auto-sets retention_expires_at = period_end + 7 years

Recent Migrations

  • 20260223150836_invoice_inbox — Invoice inbox table with document type classification, AI extraction, supplier/transaction matching, and receipt linking.
  • 20260224101905_booking_template_embeddings — Booking templates with AI embeddings for suggestion matching.
  • 20260224132419_user_description_matching — User description matching for transaction categorization.
  • 20260224165254_prevent_overlapping_fiscal_periods — Exclusion constraint preventing overlapping fiscal periods per user.
  • 20260224190818_enforce_fiscal_period_month_boundaries — Ensures fiscal periods start/end on month boundaries.
  • 20260225103139_full_bas_2026 — Full BAS 2026 account catalog, K2-excluded flag, and SRU code backfill.
  • 20260226120553_expand_account_type_untaxed_reserves — Adds untaxed_reserves to chart_of_accounts.account_type CHECK constraint for BAS 21xx accounts (obeskattade reserver).
  • 20260304191528_set_search_path_on_functions — Pins search_path = public on all 24 custom functions to prevent search_path injection.
  • 20260306084837_invoice_delivery_note_sequences — Adds delivery note number sequence and generate_invoice_number/generate_delivery_note_number RPC functions.

Type System

  • All shared types live in types/index.ts — this is the single source of truth
  • Import via import type { TypeName } from '@/types'
  • When adding new domain types, add them to types/index.ts
  • Event types are the exception — they live in lib/events/types.ts (since they reference domain types)

API Route Patterns

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 user_id (defense in depth alongside RLS)
  // Wrap journal entry creation in try/catch (non-blocking side effect)
  return NextResponse.json({ data: result })
}
  • Dynamic route params: { params }: { params: Promise<{ id: string }> } (Next.js 16)
  • Response shapes: { data } for success, { error } for failures

Skills, Git & CI

Skills: Always use /frontend-design for new UI. Use langchain for AI features. Use vercel:deploy for deployment.

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.


Deployment

Hosted on Vercel. Cron jobs in vercel.json (banking sync daily 05:00, deadlines 06:00, reminders 08:00, push notifications 09:00, tax deadlines yearly Jan 2, document verify weekly Sunday 03:00).

Core env vars: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_APP_URL, CRON_SECRET. Auth env vars: NEXT_PUBLIC_REQUIRE_MFA (set true on hosted), NEXT_PUBLIC_SELF_HOSTED (set true for Docker). Extension env vars only needed when that extension is enabled.

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 Personality

Minimal. Sharp. Efficient.

gnubok is the tool that makes accounting feel handled. It doesn't try to be fun — it earns trust through precision, clarity, and speed. The interface should feel like a well-made instrument: considered, quiet, and confident. Think Mercury's calm authority applied to Swedish bookkeeping.

Aesthetic Direction

  • Palette: Grayscale foundation with restrained semantic colors — sage green (success/balance), terracotta (errors/overdue), ochre (warnings/attention). No loud brand color. The absence of color is the brand.
  • Typography: Fraunces (serif) for display headings — adds warmth and distinction to an otherwise minimal interface. Geist (sans) for body — clean, modern, excellent for tabular data. Tabular numbers everywhere financial data appears.
  • Surfaces: White/near-white cards on light gray backgrounds. Subtle borders (60% opacity). Soft shadows that suggest depth without drama. Dark mode follows the same restraint.
  • Spacing: Generous whitespace. Let content breathe. Dense data (tables, ledgers) should use tighter spacing but never feel cramped.
  • Motion: Subtle and purposeful. Stagger animations for list entry, spring easing for interactive feedback. Never decorative — always communicates state change.
  • Icons: Lucide — consistent 15px in navigation, slightly larger in empty states and actions.
  • Reference: Mercury (banking) — calm, trustworthy, clean data presentation, financial precision.
  • Anti-reference: Enterprise software (SAP/Oracle density, overwhelming dashboards with 50 widgets, toolbar soup).

Design Principles

  1. Clarity over cleverness. Every element should be immediately understandable. Accounting is complex enough — the UI must never add confusion. Use familiar patterns, clear labels (in Swedish), and obvious hierarchy.

  2. Earned minimalism. Remove everything that doesn't serve the user's task. But don't strip away context that prevents errors — in accounting, a missing detail can mean a compliance violation. Minimal means considered, not sparse.

  3. Numbers are first-class. Financial data deserves typographic care: tabular-nums, proper alignment, adequate contrast, clear positive/negative distinction. A journal entry grid should feel as precise as a printed ledger.

  4. Trust through consistency. Same patterns, same spacing, same behavior everywhere. Buttons work the same way. Cards look the same way. When the user learns one screen, they've learned them all.

  5. Speed is a feature. Fast load, fast interaction, fast comprehension. Users are here to get a job done and leave. Optimize for the 90-second session: open app, categorize three transactions, close app.

Accessibility

  • Target: WCAG AA compliance
  • Minimum contrast ratio: 4.5:1 for text, 3:1 for UI components
  • All interactive elements keyboard-navigable with visible focus rings
  • Respect prefers-reduced-motion for users who disable animations
  • Color is never the sole indicator of state — always pair with icons, text, or shape