# Technical Architecture Document ## Stack Overview ``` Frontend: Next.js (App Router) + React + TypeScript Styling: Tailwind CSS + shadcn/ui Backend: Next.js API Routes + Supabase Edge Functions Database: Supabase (PostgreSQL) Auth: Supabase Auth (magic link + Google OAuth) Storage: Supabase Storage (invoice PDFs) Banking: Enable Banking (PSD2 AISP) PDF Gen: @react-pdf/renderer or Puppeteer Hosting: Vercel Currency: Riksbanken API for exchange rates ``` ## Project Structure ``` / ├── app/ │ ├── (auth)/ │ │ ├── login/page.tsx │ │ └── callback/page.tsx │ ├── (onboarding)/ │ │ └── onboarding/ │ │ ├── page.tsx # Wizard container │ │ ├── step-1/page.tsx # Entity type │ │ ├── step-2/page.tsx # Company details │ │ ├── step-3/page.tsx # Tax registration │ │ ├── step-4/page.tsx # Preliminary tax │ │ ├── step-5/page.tsx # Bank details for invoices │ │ └── step-6/page.tsx # Connect bank (Enable Banking) │ ├── (dashboard)/ │ │ ├── layout.tsx │ │ ├── page.tsx # Main dashboard │ │ ├── transactions/ │ │ │ └── page.tsx # Swipe categorization │ │ ├── invoices/ │ │ │ ├── page.tsx # Invoice list │ │ │ ├── new/page.tsx # Create invoice │ │ │ ├── [id]/page.tsx # Invoice detail │ │ │ └── [id]/credit/page.tsx # Create credit note │ │ ├── customers/ │ │ │ └── page.tsx # Customer management │ │ └── settings/ │ │ └── page.tsx # Company profile │ └── api/ │ ├── banking/ │ │ ├── connect/route.ts # Initiate Enable Banking connection │ │ ├── callback/route.ts # PSD2 callback │ │ └── sync/route.ts # Fetch transactions │ ├── invoices/ │ │ ├── route.ts # CRUD │ │ ├── [id]/pdf/route.ts # Generate PDF │ │ └── [id]/credit/route.ts # Create credit note │ ├── currency/ │ │ └── rates/route.ts # Riksbanken exchange rates │ └── webhooks/ │ └── banking/route.ts # Transaction webhooks ├── components/ │ ├── ui/ # shadcn components │ ├── onboarding/ │ │ ├── WizardProgress.tsx │ │ ├── EntityTypeSelector.tsx │ │ └── BankConnectButton.tsx │ ├── dashboard/ │ │ ├── BalanceCard.tsx │ │ ├── TaxBreakdown.tsx │ │ ├── FskattWarning.tsx │ │ └── RevenueChart.tsx │ ├── transactions/ │ │ ├── SwipeCard.tsx │ │ └── TransactionList.tsx │ └── invoices/ │ ├── InvoiceForm.tsx │ ├── InvoicePreview.tsx │ ├── CurrencySelector.tsx │ └── CustomerSelect.tsx ├── lib/ │ ├── supabase/ │ │ ├── client.ts # Browser client │ │ ├── server.ts # Server client │ │ └── admin.ts # Service role client │ ├── banking/ │ │ └── enable-banking.ts # Enable Banking API wrapper │ ├── currency/ │ │ └── riksbanken.ts # Exchange rate fetching │ ├── tax/ │ │ ├── calculator.ts # Swedish tax calculations │ │ ├── expense-warnings.ts # Non-deductible expense alerts │ │ ├── schablonavdrag.ts # Standard deductions │ │ └── fskatt-warning.ts # Preliminary tax comparison │ ├── invoice/ │ │ ├── vat-rules.ts # VAT logic │ │ └── pdf-generator.ts │ └── utils/ │ └── currency.ts ├── types/ │ └── index.ts # TypeScript interfaces └── supabase/ ├── migrations/ # Database migrations └── seed.sql # Test data ``` ## Authentication Flow 1. User lands on `/login` 2. Enters email → Supabase sends magic link 3. User clicks link → redirected to `/callback` 4. Callback exchanges code for session 5. **Onboarding check:** Query `company_settings` for user - If no settings exist → redirect to `/onboarding` - If settings exist but `onboarding_complete = false` → redirect to `/onboarding` - If settings complete → redirect to `/` (dashboard) Alternative: Google OAuth for faster onboarding (still requires completing wizard). ## Onboarding Flow ``` 1. User completes auth 2. Middleware checks company_settings.onboarding_complete 3. If incomplete → force redirect to /onboarding 4. Wizard steps (cannot skip): a. Entity type selection b. Company details (name, org.nr, address) c. Tax registration (F-skatt, VAT, momsperiod) d. Preliminary tax amount (optional but encouraged) e. Bank details for invoices f. Connect bank via Enable Banking (required) 5. On completion → set onboarding_complete = true 6. Redirect to dashboard ``` **Middleware Logic:** ```typescript // middleware.ts - simplified const publicRoutes = ['/login', '/auth/callback'] const onboardingRoutes = ['/onboarding'] if (!user) { // Not logged in → login page return redirect('/login') } const settings = await getCompanySettings(user.id) if (!settings?.onboarding_complete) { // Onboarding incomplete → force wizard if (!onboardingRoutes.some(r => path.startsWith(r))) { return redirect('/onboarding') } } // Onboarding complete → allow access ``` ## Bank Connection Flow ### PSD2 Provider: Enable Banking Using Enable Banking as the AISP provider. **Why Enable Banking:** - Good coverage of Swedish banks (Nordea, SEB, Handelsbanken, Swedbank, etc.) - Reasonable pricing for startups - TSP model: they hold the license, you process data - REST API with good documentation **Consent Lifecycle:** - PSD2 RTS allows 90-180 day consent periods (bank dependent) - System must track consent expiry per bank connection - Proactive notification: "Din bankkoppling till Nordea löper ut om 7 dagar" **Multiple Accounts:** - Users can connect multiple bank accounts (business + savings) - Each connection tracked separately with own consent expiry - Dashboard aggregates across all connected accounts ``` 1. User clicks "Koppla bank" 2. Frontend calls POST /api/banking/connect 3. Backend creates Enable Banking session, returns redirect URL 4. User selects bank, authenticates with BankID (SCA requirement) 5. Enable Banking redirects to /api/banking/callback 6. Backend stores connection metadata (consent expiry, account info) 7. Initial transaction fetch (90 days history typical) 8. User redirected to /transactions 9. Webhook receives new transactions in real-time 10. At consent expiry - 7 days: prompt re-consent flow ``` ### Non-Custodial Design (Critical) The app must NEVER hold user funds. This avoids: - Lagen om redovisningsmedel (1944:181) - Klientmedelskonto requirements - Full betalningsinstitut licensing For future "tax savings" feature: - Use PISP to instruct user's bank to transfer to user's OWN savings account - App orchestrates, never touches money - User maintains full control and ownership ## Currency Conversion ### Riksbanken Integration For multi-currency invoicing, convert foreign amounts to SEK for bokföring. ```typescript // lib/currency/riksbanken.ts interface ExchangeRate { currency: string rate: number date: string } const RIKSBANKEN_API = 'https://api.riksbank.se/swea/v1/CrossRates' export async function getExchangeRate( currency: 'EUR' | 'USD' | 'GBP', date: string // YYYY-MM-DD ): Promise { // Riksbanken publishes daily rates // Use booking date for invoice conversion const response = await fetch( `${RIKSBANKEN_API}/${currency}/SEK/${date}` ) const data = await response.json() return data.value } // Invoice creation: store both original and SEK amounts interface InvoiceAmounts { originalCurrency: string originalTotal: number exchangeRate: number exchangeRateDate: string sekTotal: number // This is what goes in bokföring } ``` **Bokföringslagen requirement:** Foreign currency transactions must be converted to SEK. The exchange rate on the invoice date (fakturadatum) is typically used. ## Tax Calculation Logic Swedish sole proprietor (enskild näringsidkare) tax components: ```typescript interface TaxBreakdown { grossIncome: number; // Total invoiced + received deductibleExpenses: number; // Categorized business expenses netIncome: number; // grossIncome - deductibleExpenses // Egenavgifter (self-employment contributions) ~28.97% egenavgifter: number; // Preliminary income tax (kommunalskatt ~32% avg) incomeTax: number; // VAT collected (if registered) vatCollected: number; vatDeductible: number; vatPayable: number; // Total locked totalLocked: number; // Available to spend disponibelt: number; } ``` **Important**: These are estimates. Display clear disclaimer. ## VAT Rules Engine ```typescript type VatScenario = | 'swedish_customer' // 25% VAT | 'eu_business_valid_vat' // 0% + reverse charge text | 'eu_consumer' // 25% VAT (or destination country rate for digital services) | 'non_eu' // 0% VAT, export interface VatDecision { rate: number; reverseCharge: boolean; invoiceText: string | null; // Legal text to include } ``` Validation: EU VAT numbers validated via VIES API. ## Security Considerations - Row Level Security (RLS) on all tables - User can only access own data - Banking tokens encrypted at rest (Supabase Vault or env vars) - Invoice PDFs in private bucket, signed URLs for access - No PII in logs - Rate limiting on API routes ## Environment Variables ``` # Supabase NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= # Banking (Tink example) TINK_CLIENT_ID= TINK_CLIENT_SECRET= TINK_REDIRECT_URI= # App NEXT_PUBLIC_APP_URL=https://app.yourdomain.se # PDF PDF_STORAGE_BUCKET=invoices ``` ## Deployment Vercel configuration: ```json // vercel.json { "crons": [ { "path": "/api/banking/sync", "schedule": "0 6 * * *" } ] } ``` Database migrations run via Supabase CLI or dashboard.