pargo-auth 0.1.4__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. pargo_auth-0.1.6/.github/workflows/deploy.yml +54 -0
  2. pargo_auth-0.1.6/DOCUMENTATION/AUTH_SYSTEM.md +195 -0
  3. pargo_auth-0.1.6/DOCUMENTATION/Plans/2026-02-03_apple-signin.md +110 -0
  4. pargo_auth-0.1.6/PKG-INFO +214 -0
  5. pargo_auth-0.1.6/README.md +187 -0
  6. pargo_auth-0.1.6/app/__init__.py +1 -0
  7. pargo_auth-0.1.6/app/config.py +24 -0
  8. pargo_auth-0.1.6/app/main.py +44 -0
  9. pargo_auth-0.1.6/app/routes/__init__.py +1 -0
  10. pargo_auth-0.1.6/app/routes/users.py +167 -0
  11. pargo_auth-0.1.6/app/services/__init__.py +1 -0
  12. pargo_auth-0.1.6/app/services/db.py +63 -0
  13. pargo_auth-0.1.6/app/services/user_service.py +182 -0
  14. pargo_auth-0.1.6/docker/Dockerfile +22 -0
  15. pargo_auth-0.1.6/requirements-service.txt +7 -0
  16. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/src/pargo_auth/__init__.py +1 -1
  17. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/src/pargo_auth/middleware.py +2 -2
  18. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/src/pargo_auth/models.py +12 -0
  19. pargo_auth-0.1.6/tests/__init__.py +1 -0
  20. pargo_auth-0.1.4/PKG-INFO +0 -144
  21. pargo_auth-0.1.4/README.md +0 -117
  22. pargo_auth-0.1.4/tests/__init__.py +0 -0
  23. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/.github/workflows/publish.yml +0 -0
  24. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/.gitignore +0 -0
  25. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/LICENSE +0 -0
  26. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/pyproject.toml +0 -0
  27. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/pytest.ini +0 -0
  28. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/src/pargo_auth/exceptions.py +0 -0
  29. {pargo_auth-0.1.4 → pargo_auth-0.1.6}/tests/test_middleware.py +0 -0
@@ -0,0 +1,54 @@
1
+ name: Deploy Auth Service
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main, feature/auth]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Setup SSH
16
+ run: |
17
+ mkdir -p ~/.ssh
18
+ chmod 700 ~/.ssh
19
+ echo "${{ secrets.PARGO_SERVER_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
20
+ chmod 600 ~/.ssh/id_ed25519
21
+ ssh-keyscan -H test.pargo.dk >> ~/.ssh/known_hosts
22
+
23
+ - name: Create directories
24
+ run: ssh thor@test.pargo.dk "mkdir -p /opt/pargo-auth"
25
+
26
+ - name: Deploy code
27
+ run: |
28
+ rsync -avz --delete \
29
+ --exclude '.git' \
30
+ --exclude '.github' \
31
+ --exclude '__pycache__' \
32
+ --exclude '*.pyc' \
33
+ --exclude '.env' \
34
+ --exclude 'dist' \
35
+ --exclude '*.egg-info' \
36
+ ./ thor@test.pargo.dk:/opt/pargo-auth/
37
+
38
+ - name: Build and restart container
39
+ run: |
40
+ ssh thor@test.pargo.dk "cd /opt/pargo-llm-compose && \
41
+ docker compose build --no-cache pargo-auth && \
42
+ docker compose up -d pargo-auth"
43
+
44
+ - name: Verify deployment
45
+ run: |
46
+ echo "=== Container status ==="
47
+ ssh thor@test.pargo.dk "docker ps | grep pargo-auth || true"
48
+ echo ""
49
+ echo "=== Recent logs ==="
50
+ sleep 5
51
+ ssh thor@test.pargo.dk "docker logs pargo-auth --tail 20 2>&1 || true"
52
+ echo ""
53
+ echo "=== Health check ==="
54
+ ssh thor@test.pargo.dk "curl -s http://localhost:8040/health || echo 'Health check pending...'"
@@ -0,0 +1,195 @@
1
+ # PARGO Authentication System
2
+
3
+ This document describes the authentication architecture for PARGO applications.
4
+
5
+ ## Overview
6
+
7
+ PARGO uses a hybrid authentication approach:
8
+ - **Supabase Auth** (EU-hosted) handles identity/login (Google, Apple, Email/Password)
9
+ - **pargo-auth service** validates JWTs and manages user data in our own Postgres
10
+ - **User data** is stored in Hetzner Postgres, giving us full control
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
16
+ │ Flutter │────▶│ Supabase │ │ Hetzner │
17
+ │ App │ │ Auth │ │ Postgres │
18
+ └─────────────┘ └─────────────┘ └─────────────┘
19
+ │ │ ▲
20
+ │ │ JWT │
21
+ │ ▼ │
22
+ │ ┌─────────────┐ │
23
+ └───────────▶│ pargo-auth │────────────┘
24
+ API calls │ service │ user data
25
+ └─────────────┘
26
+ ```
27
+
28
+ ## Components
29
+
30
+ ### 1. Supabase Auth (Identity Provider)
31
+ - **URL**: `https://cmnowxzhwrskkixukyif.supabase.co`
32
+ - **Purpose**: Handle login flows, issue JWTs
33
+ - **Providers**: Email/Password, Google, Apple (pending)
34
+ - **We use**: Only authentication, NOT Supabase database
35
+
36
+ ### 2. pargo-auth Service
37
+ - **URL**: `https://prod.pargo.dk/auth/`
38
+ - **Port**: 8040
39
+ - **Purpose**:
40
+ - Validate Supabase JWTs
41
+ - Provide user management API
42
+ - Store user data in our Postgres
43
+
44
+ ### 3. pargo-auth PyPI Package
45
+ - **Package**: `pargo-auth` on PyPI
46
+ - **Purpose**: JWT verification middleware for other PARGO backends
47
+ - **Usage**: `pip install pargo-auth`
48
+
49
+ ## API Endpoints
50
+
51
+ ### pargo-auth Service
52
+
53
+ | Method | Endpoint | Auth | Description |
54
+ |--------|----------|------|-------------|
55
+ | GET | `/health` | No | Health check |
56
+ | GET | `/v1/me` | Yes | Get current user profile + settings |
57
+ | PATCH | `/v1/me` | Yes | Update user profile |
58
+ | PATCH | `/v1/me/settings` | Yes | Update user settings |
59
+
60
+ ### Supabase Auth (via SDK)
61
+
62
+ | Action | SDK Method |
63
+ |--------|------------|
64
+ | Sign up | `supabase.auth.signUp()` |
65
+ | Sign in (email) | `supabase.auth.signInWithPassword()` |
66
+ | Sign in (Google) | `supabase.auth.signInWithOAuth(OAuthProvider.google)` |
67
+ | Sign in (Apple) | `supabase.auth.signInWithOAuth(OAuthProvider.apple)` |
68
+ | Sign out | `supabase.auth.signOut()` |
69
+ | Refresh token | Automatic via SDK |
70
+
71
+ ## Database Schema
72
+
73
+ User data is stored in Hetzner Postgres under the `auth` schema:
74
+
75
+ ```sql
76
+ -- Main user table
77
+ auth.users (
78
+ id UUID PRIMARY KEY,
79
+ supabase_uid TEXT UNIQUE NOT NULL, -- Links to Supabase
80
+ email TEXT,
81
+ email_verified BOOLEAN,
82
+ display_name TEXT,
83
+ avatar_url TEXT,
84
+ preferred_lang VARCHAR(5) DEFAULT 'da',
85
+ created_at TIMESTAMPTZ,
86
+ updated_at TIMESTAMPTZ,
87
+ last_login_at TIMESTAMPTZ,
88
+ deleted_at TIMESTAMPTZ -- Soft delete
89
+ )
90
+
91
+ -- User settings
92
+ auth.user_settings (
93
+ user_id UUID PRIMARY KEY REFERENCES auth.users(id),
94
+ push_enabled BOOLEAN DEFAULT TRUE,
95
+ email_enabled BOOLEAN DEFAULT TRUE,
96
+ map_style VARCHAR(20) DEFAULT 'standard',
97
+ settings_json JSONB DEFAULT '{}'
98
+ )
99
+
100
+ -- Login providers (Google, Apple, etc.)
101
+ auth.user_identities (
102
+ id UUID PRIMARY KEY,
103
+ user_id UUID REFERENCES auth.users(id),
104
+ provider VARCHAR(50), -- 'google', 'apple', 'email'
105
+ provider_uid TEXT,
106
+ email TEXT,
107
+ created_at TIMESTAMPTZ
108
+ )
109
+ ```
110
+
111
+ ## JWT Structure
112
+
113
+ Supabase JWTs contain:
114
+
115
+ ```json
116
+ {
117
+ "sub": "c2de269a-b357-4502-8efb-d92a463dda91", // Supabase user ID
118
+ "email": "user@example.com",
119
+ "app_metadata": {
120
+ "provider": "google",
121
+ "providers": ["google"]
122
+ },
123
+ "user_metadata": {
124
+ "full_name": "John Doe",
125
+ "avatar_url": "https://..."
126
+ },
127
+ "exp": 1770096078,
128
+ "iat": 1770092478
129
+ }
130
+ ```
131
+
132
+ ## Using pargo-auth in Other Services
133
+
134
+ ### Installation
135
+
136
+ ```bash
137
+ pip install pargo-auth
138
+ ```
139
+
140
+ ### Required Auth
141
+
142
+ ```python
143
+ from pargo_auth import SupabaseAuth, AuthenticatedUser
144
+ from fastapi import Depends
145
+
146
+ auth = SupabaseAuth()
147
+
148
+ @app.get("/protected")
149
+ async def protected_route(user: AuthenticatedUser = Depends(auth.get_user)):
150
+ return {"user_id": user.sub, "email": user.email}
151
+ ```
152
+
153
+ ### Optional Auth (for migration)
154
+
155
+ ```python
156
+ @app.get("/public")
157
+ async def public_route(user: AuthenticatedUser | None = Depends(auth.get_user_optional)):
158
+ if user:
159
+ return {"authenticated": True, "user_id": user.sub}
160
+ return {"authenticated": False}
161
+ ```
162
+
163
+ ## Configuration
164
+
165
+ ### Environment Variables
166
+
167
+ | Variable | Description | Example |
168
+ |----------|-------------|---------|
169
+ | `SUPABASE_URL` | Supabase project URL | `https://xxx.supabase.co` |
170
+ | `DATABASE_URL` | Postgres connection | `postgresql://user:pass@host:5432/db` |
171
+ | `ENV` | Environment | `prod` or `test` |
172
+
173
+ ### Supabase Dashboard Settings
174
+
175
+ - **Site URL**: `https://prod.pargo.dk`
176
+ - **Redirect URLs**: `https://prod.pargo.dk/**`
177
+
178
+ ## Security Notes
179
+
180
+ 1. **JWTs are validated server-side** using Supabase's JWKS endpoint
181
+ 2. **Tokens expire after 1 hour** - the SDK handles refresh automatically
182
+ 3. **User data is in our control** - Supabase only stores auth credentials
183
+ 4. **Soft deletes** preserve data for potential recovery
184
+
185
+ ## Current Status
186
+
187
+ | Provider | Status |
188
+ |----------|--------|
189
+ | Email/Password | ✅ Working |
190
+ | Google OAuth | ✅ Working |
191
+ | Apple Sign In | ⏳ Pending setup |
192
+
193
+ ---
194
+
195
+ *Last updated: 2026-02-03*
@@ -0,0 +1,110 @@
1
+ # Plan: Add Apple Sign In Support
2
+
3
+ ## Status: Pending
4
+
5
+ Apple Sign In is required for iOS App Store compliance (any app with social login must offer Apple).
6
+
7
+ ## Prerequisites
8
+
9
+ - [ ] Access to Apple Developer account
10
+ - [ ] PARGO app already registered with App ID
11
+
12
+ ## Steps
13
+
14
+ ### 1. Create Services ID in Apple Developer Console
15
+
16
+ 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list)
17
+ 2. Click **+** → Select **Services IDs** → Continue
18
+ 3. Fill in:
19
+ - Description: `PARGO Auth`
20
+ - Identifier: `dk.pargo.auth`
21
+ 4. Click **Continue** → **Register**
22
+
23
+ ### 2. Configure Services ID for Sign In with Apple
24
+
25
+ 1. Click on the Services ID you created
26
+ 2. Enable **Sign In with Apple**
27
+ 3. Click **Configure**
28
+ 4. Set:
29
+ - Primary App ID: Your PARGO app
30
+ - Domains: `cmnowxzhwrskkixukyif.supabase.co`
31
+ - Return URLs: `https://cmnowxzhwrskkixukyif.supabase.co/auth/v1/callback`
32
+ 5. Click **Save** → **Continue** → **Save**
33
+
34
+ ### 3. Create a Key for Sign In with Apple
35
+
36
+ 1. Go to **Keys** in Apple Developer
37
+ 2. Click **+**
38
+ 3. Fill in:
39
+ - Name: `PARGO Supabase Auth`
40
+ 4. Enable **Sign In with Apple**
41
+ 5. Click **Configure** → Select your Primary App ID → **Save**
42
+ 6. Click **Continue** → **Register**
43
+ 7. **IMPORTANT**: Download the `.p8` key file immediately (one-time download!)
44
+ 8. Note the **Key ID** shown
45
+
46
+ ### 4. Gather Required Information
47
+
48
+ You'll need these values for Supabase:
49
+
50
+ | Item | Where to find it | Example |
51
+ |------|------------------|---------|
52
+ | Services ID | Step 1 identifier | `dk.pargo.auth` |
53
+ | Team ID | Top-right of Apple Developer page | `ABC123XYZ` |
54
+ | Key ID | Shown after creating key | `DEFG456` |
55
+ | Private Key | Contents of `.p8` file | `-----BEGIN PRIVATE KEY-----...` |
56
+
57
+ ### 5. Configure Supabase
58
+
59
+ 1. Go to [Supabase Auth Providers](https://supabase.com/dashboard/project/cmnowxzhwrskkixukyif/auth/providers)
60
+ 2. Find **Apple** and enable it
61
+ 3. Enter:
62
+ - **Service ID**: `dk.pargo.auth`
63
+ - **Team ID**: (from Apple Developer)
64
+ - **Key ID**: (from step 3)
65
+ - **Private Key**: (paste entire `.p8` contents)
66
+ 4. Click **Save**
67
+
68
+ ### 6. Test Apple Sign In
69
+
70
+ ```bash
71
+ # Open this URL in Safari (Apple Sign In works best in Safari)
72
+ open "https://cmnowxzhwrskkixukyif.supabase.co/auth/v1/authorize?provider=apple&redirect_to=https://prod.pargo.dk/auth/v1/me"
73
+ ```
74
+
75
+ ### 7. Verify in Database
76
+
77
+ ```bash
78
+ # Check user was created
79
+ ssh thor@prod.pargo.dk "docker exec -i postgres-server psql -U postgres -d pargo_prod -c \"SELECT email, created_at FROM auth.users ORDER BY created_at DESC LIMIT 3;\""
80
+ ```
81
+
82
+ ## iOS App Configuration
83
+
84
+ For native Apple Sign In in Flutter (recommended for better UX):
85
+
86
+ 1. In Xcode, enable **Sign In with Apple** capability
87
+ 2. Add to `ios/Runner/Runner.entitlements`:
88
+ ```xml
89
+ <key>com.apple.developer.applesignin</key>
90
+ <array>
91
+ <string>Default</string>
92
+ </array>
93
+ ```
94
+
95
+ ## Notes
96
+
97
+ - Apple Sign In is **required** by App Store if you have any social login
98
+ - Apple may hide user's real email (relay address like `xxx@privaterelay.appleid.com`)
99
+ - First login returns user's name, subsequent logins may not - store it on first login
100
+ - The `.p8` key file can only be downloaded ONCE - store it securely
101
+
102
+ ## Estimated Time
103
+
104
+ - Apple Developer setup: 15-20 minutes
105
+ - Supabase configuration: 5 minutes
106
+ - Testing: 10 minutes
107
+
108
+ ---
109
+
110
+ *Created: 2026-02-03*
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: pargo-auth
3
+ Version: 0.1.6
4
+ Summary: Shared authentication middleware for PARGO backend services
5
+ Project-URL: Homepage, https://github.com/pargoorg/pargo-auth
6
+ Project-URL: Repository, https://github.com/pargoorg/pargo-auth
7
+ Author-email: PARGO <dev@pargo.dk>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: cryptography>=41.0.0
20
+ Requires-Dist: fastapi>=0.100.0
21
+ Requires-Dist: httpx>=0.24.0
22
+ Requires-Dist: pyjwt>=2.8.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pargo-auth
29
+
30
+ Authentication and user management for PARGO. This repo serves two purposes:
31
+
32
+ 1. **PyPI Package** (`pargo-auth`) - JWT verification middleware for FastAPI
33
+ 2. **User Service** - API for user profile and settings management
34
+
35
+ ## Architecture
36
+
37
+ ```
38
+ pargo-auth/
39
+ ├── src/pargo_auth/ # Library (published to PyPI)
40
+ │ ├── middleware.py # Supabase JWT verification
41
+ │ ├── models.py # AuthenticatedUser dataclass
42
+ │ └── exceptions.py # Auth errors
43
+ ├── app/ # Service (deployed to server)
44
+ │ ├── main.py # FastAPI application
45
+ │ ├── routes/users.py # /v1/me endpoints
46
+ │ └── services/ # Database operations
47
+ ├── docker/Dockerfile # Service container
48
+ └── pyproject.toml # Package configuration
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Part 1: PyPI Package
54
+
55
+ The `pargo-auth` package provides JWT verification for any PARGO backend.
56
+
57
+ ### Installation
58
+
59
+ ```bash
60
+ pip install pargo-auth
61
+ ```
62
+
63
+ ### Usage
64
+
65
+ ```python
66
+ from fastapi import Depends
67
+ from pargo_auth import get_current_user, AuthenticatedUser
68
+
69
+ @app.get("/protected")
70
+ async def protected(user: AuthenticatedUser = Depends(get_current_user)):
71
+ return {"user_id": user.sub, "email": user.email}
72
+ ```
73
+
74
+ ### Configuration
75
+
76
+ Set environment variables:
77
+
78
+ ```bash
79
+ SUPABASE_URL=https://your-project.supabase.co
80
+ ENV=local # Skip verification in local dev
81
+ ```
82
+
83
+ ### AuthenticatedUser
84
+
85
+ | Field | Type | Description |
86
+ |-------|------|-------------|
87
+ | `sub` | `str` | Supabase user ID (stable, canonical identity) |
88
+ | `email` | `str \| None` | User's email |
89
+ | `email_verified` | `bool` | Whether email is verified |
90
+ | `supabase_uid` | `str` | Alias for `sub` |
91
+ | `raw_claims` | `dict \| None` | Full JWT payload |
92
+
93
+ ### Optional Auth
94
+
95
+ For endpoints that work with or without authentication:
96
+
97
+ ```python
98
+ from pargo_auth import SupabaseAuth
99
+
100
+ auth = SupabaseAuth()
101
+
102
+ @app.get("/content")
103
+ async def get_content(user: AuthenticatedUser | None = Depends(auth.get_user_optional)):
104
+ if user:
105
+ return {"personalized": True, "user": user.sub}
106
+ return {"personalized": False}
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Part 2: User Service
112
+
113
+ The service manages user data in Hetzner Postgres (`auth` schema).
114
+
115
+ ### Endpoints
116
+
117
+ | Method | Path | Auth | Description |
118
+ |--------|------|------|-------------|
119
+ | GET | `/v1/me` | Required | Get profile + settings (creates user on first call) |
120
+ | PATCH | `/v1/me` | Required | Update profile |
121
+ | PATCH | `/v1/me/settings` | Required | Update settings |
122
+ | DELETE | `/v1/me` | Required | Soft delete account |
123
+ | GET | `/health` | None | Health check |
124
+
125
+ ### Database Schema
126
+
127
+ Uses the `auth` schema in Postgres:
128
+
129
+ - `auth.users` - Core user identity (synced from Supabase JWT)
130
+ - `auth.user_identities` - Login methods (apple, google, email)
131
+ - `auth.user_settings` - App preferences
132
+ - `auth.audit_events` - Security/login event log
133
+
134
+ ### Running Locally
135
+
136
+ ```bash
137
+ # Install dependencies
138
+ pip install -r requirements-service.txt
139
+ pip install -e .
140
+
141
+ # Set environment
142
+ export SUPABASE_URL=https://your-project.supabase.co
143
+ export DATABASE_URL=postgresql://postgres:password@localhost:5432/pargo_test
144
+ export ENV=local
145
+
146
+ # Run
147
+ uvicorn app.main:app --reload --port 8040
148
+ ```
149
+
150
+ ### Docker
151
+
152
+ ```bash
153
+ docker build -f docker/Dockerfile -t pargo-auth .
154
+ docker run -p 8040:8040 \
155
+ -e SUPABASE_URL=https://your-project.supabase.co \
156
+ -e DATABASE_URL=postgresql://... \
157
+ pargo-auth
158
+ ```
159
+
160
+ ### Response Examples
161
+
162
+ **GET /v1/me**
163
+
164
+ ```json
165
+ {
166
+ "user": {
167
+ "id": "550e8400-e29b-41d4-a716-446655440000",
168
+ "supabase_uid": "846eddd7-f5ae-4d72-8082-accde7da68f2",
169
+ "email": "user@example.com",
170
+ "email_verified": true,
171
+ "display_name": "John Doe",
172
+ "preferred_lang": "da",
173
+ "created_at": "2026-02-02T10:00:00Z",
174
+ "last_login_at": "2026-02-02T12:00:00Z"
175
+ },
176
+ "settings": {
177
+ "push_enabled": true,
178
+ "email_enabled": true,
179
+ "map_style": "standard",
180
+ "settings_json": {}
181
+ }
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Development
188
+
189
+ ### Package Development
190
+
191
+ ```bash
192
+ # Install in editable mode
193
+ pip install -e ".[dev]"
194
+
195
+ # Run tests
196
+ pytest
197
+ ```
198
+
199
+ ### Publishing (automatic)
200
+
201
+ Push to `main` triggers GitHub Actions:
202
+ 1. Bumps version
203
+ 2. Publishes to PyPI
204
+
205
+ ---
206
+
207
+ ## Design Decisions
208
+
209
+ | Decision | Rationale |
210
+ |----------|-----------|
211
+ | `supabase_uid` is canonical ID | JWT `sub` is stable even if email changes |
212
+ | User created on first `/me` call | No separate signup flow needed |
213
+ | Soft delete | GDPR compliance, account recovery possible |
214
+ | Separate settings table | Clean separation, flexible JSONB for future |