6b51d13ba7
KPI page: removed operational grid (Intäktstillväxt, Kostnadsandel, Snittbetaltid — all usually empty or redundant). Now shows 4 cards (Resultat, Kassa, Kundfordringar, Moms) + trend chart. Removed unused fields from KPIReport type and simplified API route. CLAUDE.md: documented MCP server extension, API key infrastructure, OAuth 2.1 flow, gnubok-mcp npm package, KPI page, cookieless Supabase client, and updated migration count (65 → 70). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
245 lines
14 KiB
Markdown
245 lines
14 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
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
|
||
|
||
- **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()`), cookieless service role for API key auth (`createServiceClientNoCookies()` from `lib/auth/api-keys.ts`).
|
||
- **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. Returns `403 AI_CONSENT_REQUIRED` if missing.
|
||
- **Types**: All shared types in `types/index.ts` (single source of truth). Import via `import type { T } from '@/types'`. Event types live in `lib/events/types.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/enroll` or `/mfa/verify` until AAL2
|
||
|
||
---
|
||
|
||
## Core Bookkeeping Engine
|
||
|
||
The engine (`lib/bookkeeping/engine.ts`) is the most critical system. All accounting flows route through it.
|
||
|
||
**Lifecycle**: `createDraftEntry()` → `commitEntry()` (assigns voucher number via DB RPC). Convenience: `createJournalEntry()` does both in one call. Reversal via `reverseEntry()` (storno). Correction via `correctEntry()` in `lib/core/bookkeeping/storno-service.ts`.
|
||
|
||
### 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
|
||
|
||
`VatDeclaration.breakdown.invoices` also includes `base25`/`base12`/`base6` for per-rate revenue breakdown in the UI.
|
||
|
||
---
|
||
|
||
## 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 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).
|
||
|
||
**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 calls `registerEmailService()`, core uses `getEmailService()`.
|
||
- *Services record* (ai-categorization): Extension exposes via `services` property, core looks up via `extensionRegistry.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/`): 10 tools — transactions, categorization, customers, invoices, trial balance, VAT report, KPI report, income statement. 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. 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 `user_id` (defense in depth).
|
||
|
||
**OAuth 2.1** for Claude Desktop connectors (beta — Claude's callback has a known issue, see #78):
|
||
- `.well-known/oauth-protected-resource` and `.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_codes` table
|
||
- 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.
|
||
|
||
**KPI page** (`/kpi`): 4 metrics (Resultat, Kassa, Kundfordringar, Moms) + monthly trend chart. API at `/api/reports/kpi`.
|
||
|
||
---
|
||
|
||
## API Route Pattern
|
||
|
||
```typescript
|
||
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
|
||
|
||
---
|
||
|
||
## 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()`, `createMockRequest()`, `parseJsonResponse()`, `createMockRouteParams()`, and fixture factories (`makeTransaction()`, `makeJournalEntry()`, `makeInvoice()`, `makeCustomer()`, `makeSupplier()`, `makeSupplierInvoice()`, `makeFiscalPeriod()`, `makeReceipt()`, `makeDocumentAttachment()`, `makeCompanySettings()`, `makeInvoiceInboxItem()`, 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/` — 70 files. Early migrations use sequential numbering (`20240101000001`–`20240101000038`), later ones use real timestamps.
|
||
|
||
### 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`
|
||
|
||
---
|
||
|
||
## 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 defined in `vercel.json` (banking sync, deadlines, reminders, tax deadlines, document verification, sandbox cleanup).
|
||
|
||
**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 & 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
|
||
|
||
1. **Clarity over cleverness.** Every element immediately understandable. Clear labels (in Swedish), obvious hierarchy.
|
||
2. **Earned minimalism.** Remove what doesn't serve the task, but don't strip context that prevents compliance errors.
|
||
3. **Numbers are first-class.** Tabular-nums, proper alignment, adequate contrast, clear positive/negative distinction.
|
||
4. **Trust through consistency.** Same patterns, spacing, and behavior everywhere.
|
||
5. **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
|