···1111| `PORT` | No | `8000` | Server port |
1212| `SECRET_KEY_BASE` | Recommended | Auto-generated | Session encryption key (64+ chars). **Must persist across restarts** |
1313| `ADMIN_DIDS` | Optional | - | Comma-separated DIDs for admin access (e.g., `did:plc:abc,did:plc:xyz`) |
1414-| `OAUTH_CLIENT_ID` | Optional | - | OAuth client ID |
1515-| `OAUTH_CLIENT_SECRET` | Optional | - | OAuth client secret |
1414+| `ENABLE_OAUTH_AUTO_REGISTER` | Optional | `false` | Enable automatic OAuth client registration with AIP on server boot |
1515+| `OAUTH_CLIENT_ID` | Optional | - | OAuth client ID (auto-registered if `ENABLE_OAUTH_AUTO_REGISTER=true`) |
1616+| `OAUTH_CLIENT_SECRET` | Optional | - | OAuth client secret (auto-registered if `ENABLE_OAUTH_AUTO_REGISTER=true`) |
1617| `OAUTH_REDIRECT_URI` | Optional | `http://localhost:8000/oauth/callback` | OAuth callback URL |
1718| `AIP_BASE_URL` | Optional | `https://auth.example.com` | AT Protocol Identity Provider URL |
1819| `JETSTREAM_URL` | No | `wss://jetstream2.us-west.bsky.network/subscribe` | Jetstream WebSocket endpoint |
···2425- **DATABASE_URL**: Must point to a persistent volume location
2526- **SECRET_KEY_BASE**: Generate with `openssl rand -base64 48`. Store as a secret and keep persistent
2627- **HOST**: Set to `0.0.0.0` in container environments
2727-- **ADMIN_DIDS**: Required for backfill
2828+- **ADMIN_DIDS**: Required for backfill and settings page access
2929+3030+### OAuth Configuration
3131+3232+Quickslice supports two approaches for OAuth configuration:
3333+3434+#### Option A: Auto-Registration (Recommended)
3535+3636+Set `ENABLE_OAUTH_AUTO_REGISTER=true` to automatically register an OAuth client with your AIP server on startup. The server will:
3737+3838+1. Check if OAuth credentials exist in the database
3939+2. If not found, automatically register with the AIP server at `/oauth/clients/register`
4040+3. Store the client ID and secret in the database for future use
4141+4. Retry with exponential backoff (2, 4, 8... up to 30 minutes) if registration fails
4242+4343+**Benefits:**
4444+- No manual OAuth client setup required
4545+- Credentials persist in the database across restarts
4646+- Automatic retry if AIP server is temporarily unavailable
4747+4848+**Requirements:**
4949+- `AIP_BASE_URL` must be set to your AIP server URL
5050+- AIP server must support dynamic client registration (RFC 7591)
5151+5252+#### Option B: Manual Configuration
5353+5454+Alternatively, you can manually register an OAuth client with your AIP server and provide the credentials via environment variables:
5555+5656+- `OAUTH_CLIENT_ID`: Your pre-registered client ID
5757+- `OAUTH_CLIENT_SECRET`: Your pre-registered client secret
5858+5959+**Note:** Manual credentials take precedence over auto-registered credentials.
28602961## SQLite Volume Setup
3062···81113```bash
82114fly secrets set SECRET_KEY_BASE=$(openssl rand -base64 48)
831158484-# Optional: OAuth configuration
116116+# Optional: Admin access
117117+fly secrets set ADMIN_DIDS=did:plc:your_did
118118+119119+# OAuth configuration (choose one approach):
120120+121121+# Option A: Auto-registration (recommended)
122122+# Automatically registers OAuth client with AIP on startup
123123+fly secrets set ENABLE_OAUTH_AUTO_REGISTER=true
124124+fly secrets set AIP_BASE_URL=https://your-aip-server.com
125125+126126+# Option B: Manual configuration
127127+# Use pre-existing OAuth client credentials
85128fly secrets set OAUTH_CLIENT_ID=your_client_id
86129fly secrets set OAUTH_CLIENT_SECRET=your_client_secret
8787-8888-# Optional: Admin access
8989-fly secrets set ADMIN_DIDS=did:plc:your_did
90130```
9113192132### 4. Deploy
···122162Optional variables:
123163```
124164ADMIN_DIDS=did:plc:your_did
125125-OAUTH_CLIENT_ID=your_client_id
126126-OAUTH_CLIENT_SECRET=your_client_secret
165165+166166+# OAuth - Option A: Auto-registration (recommended)
167167+ENABLE_OAUTH_AUTO_REGISTER=true
168168+AIP_BASE_URL=https://your-aip-server.com
127169OAUTH_REDIRECT_URI=https://your-app.up.railway.app/oauth/callback
170170+171171+# OAuth - Option B: Manual configuration
172172+# OAUTH_CLIENT_ID=your_client_id
173173+# OAUTH_CLIENT_SECRET=your_client_secret
174174+# OAUTH_REDIRECT_URI=https://your-app.up.railway.app/oauth/callback
128175```
129176130177### 3. Add a volume
···185232 - DATABASE_URL=/data/quickslice.db
186233 - SECRET_KEY_BASE=${SECRET_KEY_BASE}
187234 - ADMIN_DIDS=${ADMIN_DIDS}
235235+ # OAuth auto-registration (recommended)
236236+ - ENABLE_OAUTH_AUTO_REGISTER=${ENABLE_OAUTH_AUTO_REGISTER:-false}
237237+ - AIP_BASE_URL=${AIP_BASE_URL}
238238+ # Or use manual OAuth configuration
239239+ # - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
240240+ # - OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
188241 restart: unless-stopped
189242 healthcheck:
190243 test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/health"]
···201254```bash
202255SECRET_KEY_BASE=<generate-with-openssl-rand>
203256ADMIN_DIDS=did:plc:your_did
257257+258258+# OAuth auto-registration (recommended)
259259+ENABLE_OAUTH_AUTO_REGISTER=true
260260+AIP_BASE_URL=https://your-aip-server.com
261261+262262+# Or use manual OAuth configuration
263263+# OAUTH_CLIENT_ID=your_client_id
264264+# OAUTH_CLIENT_SECRET=your_client_secret
204265```
205266206267Start the service:
+12-4
server/.env.example
···66# IMPORTANT: This MUST be persistent across server restarts or sessions will be invalidated
77SECRET_KEY_BASE=CHANGE_ME_TO_A_RANDOM_64_CHARACTER_STRING
8899-# OAuth Client Credentials (obtained from your OAuth provider)
1010-OAUTH_CLIENT_ID=your_client_id
1111-OAUTH_CLIENT_SECRET=your_client_secret
99+# OAuth Auto-Registration (recommended)
1010+# When enabled, the server will automatically register as an OAuth client with your AIP server
1111+# on startup if no credentials exist. Credentials are stored in the database config table.
1212+ENABLE_OAUTH_AUTO_REGISTER=true
1313+1414+# OAuth Client Credentials (manual configuration - optional)
1515+# If you set these environment variables, they will override auto-registered credentials.
1616+# You can leave these commented out if using auto-registration.
1717+# OAUTH_CLIENT_ID=your_client_id
1818+# OAUTH_CLIENT_SECRET=your_client_secret
12191320# OAuth Redirect URI (must match the URI registered with your OAuth provider)
1414-OAUTH_REDIRECT_URI=http://localhost:8000/oauth/callback
2121+# Defaults to {EXTERNAL_BASE}/oauth/callback if not set
2222+# OAUTH_REDIRECT_URI=http://localhost:8000/oauth/callback
15231624# Admin DIDs (comma-separated list of DIDs that have admin access to backfill, GraphiQL, etc.)
1725ADMIN_DIDS=did:plc:example1,did:plc:example2
+44
server/src/database.gleam
···371371 Ok(Nil)
372372}
373373374374+/// Get OAuth client credentials from config table
375375+/// Returns a tuple of (client_id, client_secret, redirect_uri) if all values exist
376376+pub fn get_oauth_credentials(
377377+ conn: sqlight.Connection,
378378+) -> Result(Option(#(String, String, String)), sqlight.Error) {
379379+ case get_config(conn, "oauth_client_id"), get_config(conn, "oauth_client_secret") {
380380+ Ok(client_id), Ok(client_secret) -> {
381381+ let redirect_uri = case get_config(conn, "oauth_redirect_uri") {
382382+ Ok(uri) -> uri
383383+ Error(_) -> ""
384384+ }
385385+ Ok(Some(#(client_id, client_secret, redirect_uri)))
386386+ }
387387+ Error(_), _ -> Ok(None)
388388+ _, Error(_) -> Ok(None)
389389+ }
390390+}
391391+392392+/// Delete OAuth credentials from config table
393393+pub fn delete_oauth_credentials(
394394+ conn: sqlight.Connection,
395395+) -> Result(Nil, sqlight.Error) {
396396+ use _ <- result.try(delete_config(conn, "oauth_client_id"))
397397+ use _ <- result.try(delete_config(conn, "oauth_client_secret"))
398398+ use _ <- result.try(delete_config(conn, "oauth_redirect_uri"))
399399+ Ok(Nil)
400400+}
401401+402402+/// Generic config deletion function
403403+fn delete_config(
404404+ conn: sqlight.Connection,
405405+ key: String,
406406+) -> Result(Nil, sqlight.Error) {
407407+ let sql = "DELETE FROM config WHERE key = ?"
408408+409409+ use _ <- result.try(sqlight.query(
410410+ sql,
411411+ on: conn,
412412+ with: [sqlight.text(key)],
413413+ expecting: decode.string,
414414+ ))
415415+ Ok(Nil)
416416+}
417417+374418/// Deletes all lexicons from the database
375419pub fn delete_all_lexicons(
376420 conn: sqlight.Connection,