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:

bash
openssl rand -hex 32

Copy the output. It will look like:

code
a3f8e2c1d94b07f65e3a18c29d04b72f1e6d83a0c59f2b47e81d06c3a94f51e

Keep it on your clipboard. Do not write it down anywhere unencrypted.

---

Step 2. Add STORY_API_KEY to Vercel

Option A — Vercel CLI (recommended)

bash
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 key

Verify it was set:

bash
vercel env ls production | grep STORY_API_KEY

Option 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

bash
# In /Users/UNPARTYLLC/theunpartyapp/.env.local
echo "STORY_API_KEY=<your-key>" >> .env.local

Verify .env.local is in .gitignore (it should be by default in Next.js):

bash
grep ".env.local" /Users/UNPARTYLLC/theunpartyapp/.gitignore

If it is not listed, add it:

bash
echo ".env.local" >> /Users/UNPARTYLLC/theunpartyapp/.gitignore

Never commit .env.local to git.

---

Step 4. Redeploy to pick up the new env var

bash
vercel --prod

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

bash
# 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

LocationStorage methodRisk if compromised
VercelEncrypted env var, never in logsRotatable in under 2 min
.env.localLocal only, gitignoredLocal machine only
iOS appKeychain, kSecAttrAccessibleWhenUnlockedRequires device unlock to read
Git historyMust never appearPermanent — 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:

bash
# 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 → Save

The 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

VariableWhere it livesWhat it does
STORY_API_KEYVercel env + .env.localGates /api/stories GET and PUT
GITHUB_APP_PRIVATE_KEYVercel env (already set)Authenticates GitHub App for commits
WAITLIST_REPO_OWNERVercel env (already set)GitHub org for commits (unparty-app)
STORIES_REPO_NAMEOptional override, defaults to theunpartyappTarget 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.tscommitStoryFile() implementation

unseen/Models/KeychainHelper.swift — iOS Keychain wrapper

unseen/Models/Story.swiftStoryAPIClient implementation

#auth#vercel#ios#security#stories#unseen

🧗🏾‍♂️ in progress

THOUGHTS.