AUTH.

How to connect theunpartyunseen to unparty.app for story editing.

---

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

Location
Vercel
Storage method
Encrypted env var, never in logs
Risk if compromised
Rotatable in under 2 min

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: propose a change to any story-*.md file in unparty-app/theunpartyapp, as a pull request that merges on its own once the checks pass

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 opens a pull request with the message story: update story-NNN via unseen, which merges on its own once the checks pass. The save dates the story first: updated-at becomes the day you saved it when its title or words changed, and stays put when you only changed tags, cover or published. Put [skip stamp] in the message to leave the date alone. 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

Variable
STORY_API_KEY
Where it lives
Vercel env + .env.local
What it does
Gates /api/stories GET and PUT

---

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