AUTH. START
How to connect theunpartyunseen to unparty.app for story editing — securely, with minimal surface area.
---
What STORY_API_KEY is
A single shared secret that gates the /api/stories read and write endpoints. The iOS app sends it as a Bearer token; the server validates it before touching any story file or committing to GitHub. It is not a GitHub token — it is a key you generate, own, and rotate.
---
Step 1. Generate the key
Run this in your terminal to produce a cryptographically random 32-byte hex string:
openssl rand -hex 32Copy the output. It will look like:
a3f8e2c1d94b07f65e3a18c29d04b72f1e6d83a0c59f2b47e81d06c3a94f51eKeep it on your clipboard. Do not write it down anywhere unencrypted.
---
Step 2. Add STORY_API_KEY to Vercel
Option A — Vercel CLI (recommended)
cd /Users/UNPARTYLLC/theunpartyapp
# Add to all three environments at once
vercel env add STORY_API_KEY production
# Paste the key when prompted
vercel env add STORY_API_KEY preview
# Paste the same key
vercel env add STORY_API_KEY development
# Paste the same keyVerify it was set:
vercel env ls production | grep STORY_API_KEYOption B — Vercel Dashboard
1. Go to vercel.com → theunpartyapp → Settings → Environment Variables
2. Click Add New
3. Name: STORY_API_KEY
4. Value: paste your generated key
5. Environments: check Production, Preview, Development
6. Click Save
---
Step 3. Add to .env.local for local development
# In /Users/UNPARTYLLC/theunpartyapp/.env.local
echo "STORY_API_KEY=<your-key>" >> .env.localVerify .env.local is in .gitignore (it should be by default in Next.js):
grep ".env.local" /Users/UNPARTYLLC/theunpartyapp/.gitignoreIf it is not listed, add it:
echo ".env.local" >> /Users/UNPARTYLLC/theunpartyapp/.gitignoreNever commit .env.local to git.
---
Step 4. Redeploy to pick up the new env var
vercel --prodOr push a commit — Vercel will redeploy automatically. The new env var takes effect on the next deployment; existing deployments do not retroactively get it.
---
Step 5. Enter the key in the iOS app (theunpartyunseen)
1. Build and run theunpartyunseen in the simulator or on device
2. In the sidebar, tap STORIES.
3. The API Key Setup sheet appears automatically on first launch (because the Keychain is empty)
4. Paste your STORY_API_KEY value into the secure field
5. Tap Save — the key is stored in the device Keychain under kSecAttrService = "app.unparty.unseen", kSecAttrAccount = "story-api-key"
6. The story list will load immediately
To update or rotate the key later: tap the key icon in the STORIES. toolbar and paste the new value.
---
Step 6. Smoke test the connection
With the dev server running (npm run dev in theunpartyapp):
# List all stories — should return JSON array
curl -s \
-H "Authorization: Bearer <your-key>" \
http://localhost:3000/api/stories | jq '.[0]'
# Fetch a single story — should return markdown text
curl -s \
-H "Authorization: Bearer <your-key>" \
http://localhost:3000/api/stories/story-001
# Test an unauthorized request — should return 401
curl -s http://localhost:3000/api/stories | jq '.error'
# Expected: "Unauthorized"---
Privacy-first best practices
On key strength and storage
| Location | Storage method | Risk if compromised |
|---|---|---|
| Vercel | Encrypted env var, never in logs | Rotatable in under 2 min |
.env.local | Local only, gitignored | Local machine only |
| iOS app | Keychain, kSecAttrAccessibleWhenUnlocked | Requires device unlock to read |
| Git history | Must never appear | Permanent — rotate immediately if it does |
The key is stored in the iOS Keychain with kSecAttrAccessibleWhenUnlocked — it cannot be read when the device is locked or by other apps. It is not stored in UserDefaults or any plist.
On access scope
STORY_API_KEY grants:
Read: all stories including unpublished drafts
Write: commit any story-*.md file to main in unparty-app/theunpartyapp
It does not grant:
Access to the Neon/Postgres database
Access to Clerk user data
Access to any file outside public/covers/stories/
The ability to delete files (GitHub's update API requires the current SHA — deletes are a separate call not implemented)
This is least-privilege by design: the key does exactly one job.
On HTTPS
All requests from the iOS app go to https://unparty.app — TLS terminates at Vercel's edge. The Bearer token is never sent in plaintext. Do not test against http:// endpoints in production.
On audit trail
Every story write creates a GitHub commit with the message story: update story-NNN via unseen. The full commit history in unparty-app/theunpartyapp is your audit log — who changed what, and when. GitHub's commit log is append-only and tamper-evident.
On key rotation
Rotate STORY_API_KEY if:
You suspect it was exposed (e.g., appeared in a log, was pasted somewhere)
You share access with someone who no longer needs it
You rotate secrets on a regular schedule (recommended: every 90 days)
Rotation procedure:
# 1. Generate a new key
openssl rand -hex 32
# 2. Remove the old key from Vercel
vercel env rm STORY_API_KEY production
vercel env rm STORY_API_KEY preview
vercel env rm STORY_API_KEY development
# 3. Add the new key
vercel env add STORY_API_KEY production # paste new key
vercel env add STORY_API_KEY preview # paste new key
vercel env add STORY_API_KEY development # paste new key
# 4. Redeploy
vercel --prod
# 5. Update the key in the iOS app
# Tap the key icon in STORIES. → paste new key → SaveThe old key stops working the moment the new deployment is live (~30s). There is no overlap window where both keys are valid.
On rate limiting (future)
The current implementation has no rate limiting on /api/stories. For an internal single-user app this is acceptable. If the surface area grows, add rate limiting at the Vercel Edge (via vercel.json or middleware) before adding more users.
On what not to store in stories
Story markdown files live in a public GitHub repo (public/covers/stories/). They are committed to main and served at unparty.app/story/*. Do not write:
Personal contact information
Draft content intended to remain private indefinitely (use published: false as a temporary hold, not a permanent privacy mechanism)
Credentials, tokens, or keys in story body text
---
Reference
| Variable | Where it lives | What it does |
|---|---|---|
STORY_API_KEY | Vercel env + .env.local | Gates /api/stories GET and PUT |
GITHUB_APP_PRIVATE_KEY | Vercel env (already set) | Authenticates GitHub App for commits |
WAITLIST_REPO_OWNER | Vercel env (already set) | GitHub org for commits (unparty-app) |
STORIES_REPO_NAME | Optional override, defaults to theunpartyapp | Target repo for story commits |
---
See also
src/app/api/stories/route.ts — GET list endpoint
src/app/api/stories/[storyId]/route.ts — GET single + PUT write endpoint
src/lib/github.ts — commitStoryFile() implementation
unseen/Models/KeychainHelper.swift — iOS Keychain wrapper
unseen/Models/Story.swift — StoryAPIClient implementation