# Registration & Onboarding Flow

# Registration & Onboarding Flow -- Low-Level Design

**Document:** LLD-REGISTRATION
**Status:** Approved
**Last updated:** 2026-02-21
**Author:** Standards Architect
**Applies to:** Drop v1.0 (PSD2 pass-through model)
**User requirements:** Minimum age 18, Norwegian residency, valid BankID (from vilkar.html)

---

## Overview

Drop's registration is simplified by using BankID as the sole authentication provider. There is no separate registration form -- user accounts are created automatically on first BankID login. The onboarding flow then guides the user through consent collection, KYC verification, and first bank account linking.

**Key principle:** Progressive disclosure. Users see only what they need at each step. Heavy verification happens in the background while the user explores the app.

---

## Complete Registration Flow

```mermaid
sequenceDiagram
    participant User
    participant App as Drop App
    participant BFF as Next.js BFF / Hono API
    participant BankID as BankID OIDC
    participant Sumsub
    participant Bank as Nordic Bank (Open Banking)
    participant DB as PostgreSQL

    Note over User,DB: Step 1 -- BankID Authentication + Auto-Registration
    User->>App: Tap "Logg inn med BankID"
    App->>BFF: GET /api/auth/bankid (or /v1/auth/bankid/initiate)
    BFF->>BFF: Generate state + nonce
    BFF->>BFF: Rate limit check (10/min per IP)
    BFF->>App: { redirectUrl }
    App->>BankID: Open BankID authorize URL

    User->>BankID: Authenticate (BankID app/code device)
    Note over User,BankID: SCA: possession (device) + knowledge (PIN)

    BankID->>App: Redirect with ?code=&state=
    App->>BFF: GET /callback?code=&state= (or POST /callback)
    BFF->>BFF: Verify state vs cookie/session
    BFF->>BankID: POST /token (exchange code)
    BankID->>BFF: { id_token, access_token }
    BFF->>BFF: Verify ID token signature (JWKS)
    BFF->>BFF: Extract pid (fodselsnummer, 11 digits)
    BFF->>BFF: Parse DOB from pid
    BFF->>BFF: Verify age >= 18

    alt Age < 18
        BFF->>App: Error: "Du ma vaere minst 18 ar"
        App->>User: Show age restriction message
    else Age >= 18
        BFF->>BFF: SHA-256 hash pid -> national_id_hash
        BFF->>DB: SELECT user WHERE national_id_hash = ?

        alt Existing user
            BFF->>DB: Create session + JWT
            BFF->>App: Set cookie / return Bearer token
            App->>User: Redirect to /dashboard
        else New user (first login)
            BFF->>DB: INSERT user (kyc_status='approved', kyc_method='bankid', auth_provider='bankid', password_hash='EIDONLY')
            BFF->>DB: Create session + JWT
            BFF->>App: Set cookie / return Bearer token
            App->>User: Redirect to /onboarding
        end
    end

    Note over User,DB: Step 2 -- Consent Collection (Onboarding Screen 1)
    App->>User: Show consent checkboxes
    User->>App: Accept terms + privacy + data processing
    App->>BFF: POST /api/consents (type: 'terms', granted: true)
    BFF->>DB: INSERT consents (terms, ip_address, granted_at)
    App->>BFF: POST /api/consents (type: 'privacy', granted: true)
    BFF->>DB: INSERT consents (privacy, ip_address, granted_at)
    App->>User: Optional: marketing consent checkbox
    Note over User,App: Marketing consent is OPTIONAL per GDPR

    Note over User,DB: Step 3 -- KYC Trigger (Background)
    BFF->>Sumsub: Create applicant (name, DOB from BankID)
    Sumsub->>BFF: applicant_id
    BFF->>DB: Store applicant_id
    Sumsub->>Sumsub: PEP + sanctions screening
    Sumsub->>BFF: Webhook: screening results
    BFF->>DB: INSERT screening_results

    Note over User,DB: Step 4 -- Bank Account Linking (Onboarding Screen 2)
    App->>User: "Koble til bankkonto"
    User->>App: Select bank (DNB, SpareBank1, Nordea...)
    App->>BFF: POST /api/bank-accounts/link
    BFF->>Bank: Open Banking: AISP consent request
    Bank->>User: Authorize AISP access (SCA)
    User->>Bank: Approve
    Bank->>BFF: AISP access token + account list
    BFF->>DB: INSERT bank_accounts (from AISP response)
    BFF->>Bank: GET /accounts/{id}/balances
    Bank->>BFF: { balance, currency }
    BFF->>DB: UPDATE bank_accounts SET balance = ?, is_primary = 1
    BFF->>App: { bankAccounts: [...] }
    App->>User: Show linked account with balance

    Note over User,DB: Step 5 -- Onboarding Complete
    App->>User: "Velkommen til Drop!"
    App->>User: Redirect to /dashboard
```

---

## Onboarding States

```mermaid
stateDiagram-v2
    [*] --> bankid_redirect : User taps "Logg inn med BankID"

    bankid_redirect --> bankid_auth : BankID authorize page
    bankid_auth --> age_check : BankID callback received

    age_check --> rejected_underage : Age < 18
    age_check --> user_lookup : Age >= 18

    rejected_underage --> [*]

    user_lookup --> existing_user : national_id_hash found
    user_lookup --> new_user : national_id_hash not found

    existing_user --> dashboard : Session created, redirect

    new_user --> user_created : Auto-register from BankID data
    user_created --> consent_collection : Redirect to /onboarding

    consent_collection --> consents_granted : Terms + privacy accepted
    consents_granted --> kyc_background : Sumsub screening starts

    kyc_background --> bank_linking : Screening runs in background
    bank_linking --> bank_consent : User selects bank
    bank_consent --> bank_authorized : AISP consent granted
    bank_authorized --> account_linked : Balance fetched

    account_linked --> onboarding_complete : First bank account linked
    onboarding_complete --> dashboard : Redirect to /dashboard

    state kyc_background {
        [*] --> sumsub_pending
        sumsub_pending --> screening
        screening --> kyc_approved : All clear
        screening --> kyc_review : Match found
        kyc_review --> kyc_approved : Cleared by compliance
        kyc_review --> kyc_rejected : Confirmed risk
    }

    dashboard --> [*]
```

---

## Age Verification (18+)

Norwegian fodselsnummer (11-digit personal identification number) encodes the date of birth:

| Digits | Meaning | Example |
|--------|---------|---------|
| 1-2 | Day of birth (DD) | `15` |
| 3-4 | Month of birth (MM) | `03` |
| 5-6 | Year of birth (YY) | `95` |
| 7-9 | Individual number | `123` |
| 10-11 | Check digits | `45` |

**Century determination** (from individual number, digits 7-9):
- 000-499: born 1900-1999
- 500-749: born 1854-1899 (historical) or 2000-2039
- 750-899: born 1854-1899 (historical)
- 900-999: born 1940-1999

**Verification logic:**
```
1. Extract DD, MM, YY from pid[0..5]
2. Determine century from pid[6..8]
3. Construct full birthdate
4. Calculate age = today - birthdate
5. If age < 18: reject with "Du ma vaere minst 18 ar for a bruke Drop"
```

**Source:** vilkar.html section 3 -- "Du ma vaere minst 18 ar og bosatt i Norge for a bruke Drop."

---

## Norwegian Residency Check

BankID issuance inherently confirms Norwegian residency:
- BankID is issued only by Norwegian banks to their customers
- Norwegian bank accounts require Norwegian national ID (fodselsnummer or D-number)
- D-numbers are issued to foreign nationals with legitimate ties to Norway

**Additional signals:**
- Phone number prefix: `+47` (Norwegian)
- BankID issuer: Norwegian bank (from ID token `amr` claim)

| Signal | Check | Enforcement |
|--------|-------|-------------|
| BankID ownership | Implicit -- only Norwegian residents have BankID | At authentication |
| Phone number | `+47` prefix | Optional validation at profile update |
| Postal address | Norwegian postal code | At bank account linking (from AISP data) |

---

## Consent Collection

### Required Consents

Per GDPR Article 6(1)(a) and Article 7, explicit consent must be collected before processing personal data. The following consents are collected during onboarding:

| Consent Type | Required | Legal Basis | Description | Withdrawable |
|-------------|----------|-------------|-------------|-------------|
| `terms` | **Yes** (mandatory) | Contract (Art. 6(1)(b)) | Terms of service acceptance | Account deletion required |
| `privacy` | **Yes** (mandatory) | Consent (Art. 6(1)(a)) | Privacy policy acknowledgment | Account deletion required |
| `data_processing` | **Yes** (mandatory) | Consent (Art. 6(1)(a)) | PSD2 AISP/PISP data processing consent | Revokes bank access |
| `marketing` | No (optional) | Consent (Art. 6(1)(a)) | Marketing communications | Yes, at any time |
| `cookies_analytics` | No (optional) | Consent (Art. 6(1)(a)) | Analytics cookies | Yes, at any time |
| `cookies_marketing` | No (optional) | Consent (Art. 6(1)(a)) | Marketing/tracking cookies | Yes, at any time |

### Consent Checklist

| # | Consent | Checkbox Text (Norwegian) | Default | Validation |
|---|---------|--------------------------|---------|------------|
| 1 | Terms of service | "Jeg godtar Drop sine brukervilkar" | Unchecked | Must be checked to proceed |
| 2 | Privacy policy | "Jeg har lest og godtar personvernerklaringen" | Unchecked | Must be checked to proceed |
| 3 | PSD2 data access | "Jeg godtar at Drop leser kontoinformasjon og initierer betalinger via Open Banking" | Unchecked | Must be checked to proceed |
| 4 | Marketing | "Jeg onsker a motta nyheter og tilbud fra Drop" | Unchecked | Optional |

### Consent Storage

Each consent is stored in the `consents` table:

| Column | Value | Purpose |
|--------|-------|---------|
| `id` | `con_<hex16>` | Unique consent record ID |
| `user_id` | `usr_<hex16>` | References the user |
| `consent_type` | `terms`, `privacy`, `marketing`, etc. | Type of consent |
| `granted` | `1` (true) or `0` (false) | Current consent state |
| `granted_at` | ISO timestamp | When consent was granted |
| `withdrawn_at` | ISO timestamp or NULL | When consent was withdrawn |
| `ip_address` | Client IP | Proof of consent action (GDPR Art. 7(1)) |

### Consent API

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `GET /api/consents` | GET | List all user consents |
| `POST /api/consents` | POST | Grant or withdraw consent |

**Consent withdrawal flow:**
1. User navigates to Settings > Privacy
2. Toggles marketing consent off
3. `POST /api/consents` with `{ consentType: "marketing", granted: false }`
4. Record updated: `granted = 0`, `withdrawn_at = now()`
5. User can re-grant at any time

---

## First Bank Account Linking

After consent collection, the user links their first bank account via AISP:

```mermaid
sequenceDiagram
    participant User
    participant App as Drop App
    participant BFF as Drop API
    participant Bank as Nordic Bank

    User->>App: Select bank from list (DNB, SpareBank1, etc.)
    App->>BFF: POST /api/bank-accounts/link { bankId: "dnb" }

    BFF->>Bank: Open Banking: GET /authorize (AISP scope)
    Bank->>User: Show consent screen (SCA required)
    Note over User,Bank: "Drop vil lese kontosaldo og<br/>transaksjonshistorikk"

    User->>Bank: Approve (BankID in banking app)
    Bank->>BFF: Authorization code
    BFF->>Bank: POST /token (exchange code)
    Bank->>BFF: AISP access token (90-day validity)

    BFF->>Bank: GET /accounts (list user accounts)
    Bank->>BFF: [{ accountId, iban, name, type }]

    BFF->>Bank: GET /accounts/{id}/balances
    Bank->>BFF: { balance: 45230.00, currency: "NOK" }

    BFF->>BFF: INSERT bank_accounts
    BFF->>BFF: Set first account as is_primary = 1

    BFF->>App: { bankAccounts: [{ bankName: "DNB", balance: 45230, isPrimary: true }] }
    App->>User: "DNB koblet! Saldo: 45 230 kr"
```

---

## Progressive Disclosure UX

The onboarding flow uses progressive disclosure to minimize upfront friction:

| Step | Screen | User Action | Background Action |
|------|--------|-------------|-------------------|
| 1 | BankID login | Tap "Logg inn med BankID" | Auto-create account from BankID data |
| 2 | Consent | Check 3 mandatory boxes + optional marketing | Record consents with IP + timestamp |
| 3 | Link bank | Select bank, approve AISP | Sumsub KYC screening runs in parallel |
| 4 | Dashboard | Explore the app | KYC result webhook updates user status |

**Time to first value:** ~2 minutes (BankID auth + consents + bank linking)

**Deferred actions** (not required during onboarding):
- Add remittance recipients (done when user first sends money)
- Merchant registration (done from Settings if user is a business)
- Notification preferences (defaults: push enabled, email enabled)
- Profile completion (BankID provides name and DOB; address is optional)

---

## Error Handling

| Error | Screen | User Message | Action |
|-------|--------|-------------|--------|
| BankID timeout | Login | "BankID-tidsavbrudd. Prov igjen." | Retry button |
| Age < 18 | Login | "Du ma vaere minst 18 ar for a bruke Drop." | No retry -- explain policy |
| BankID state mismatch | Login | "Noe gikk galt. Prov igjen." | Redirect to login |
| Consent not accepted | Onboarding | Cannot proceed without mandatory consents | Highlight unchecked boxes |
| Bank linking failed | Onboarding | "Kunne ikke koble banken. Prov igjen." | Retry or skip (link later) |
| Sumsub KYC rejected | Background | "Identitetsbekreftelse mislyktes. Kontakt oss." | Account limited until resolved |

---

## Cross-References

- [KYC/AML Flow](flow-kyc-aml.md) -- Detailed KYC verification and AML monitoring
- [Login Authentication Flow](flow-login-authentication.md) -- BankID login implementation details
- [Bank Account Linking Flow](flow-bank-account-linking.md) -- AISP integration details
- [Authentication System](../../backend/AUTHENTICATION.md) -- JWT, sessions, BankID OIDC
- [BankID OIDC Integration](../integration/bankid-oidc-integration.md) -- BankID technical specification
- [API Reference](../../backend/API-REFERENCE.md) -- Consent and auth endpoints
- [Database Schema](../../backend/DATABASE-SCHEMA.md) -- users, consents, bank_accounts tables
- [ADR-007: BankID OIDC Auth](../adr/ADR-007-bankid-oidc-auth.md) -- Authentication provider decision
- [ADR-003: PSD2 Pass-through](../adr/ADR-003-psd2-pass-through.md) -- Pass-through model context