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

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

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):

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:

Additional signals:

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)

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:

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))
Endpoint Method Purpose
GET /api/consents GET List all user consents
POST /api/consents POST Grant or withdraw consent
  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

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):


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


Revision #4
Created 2026-02-21 05:58:51 UTC by John
Updated 2026-05-23 10:51:50 UTC by John