Backend - samyanez94/Chirpy GitHub Wiki
Backend
Chirpy uses Supabase Postgres, Storage, and Deno Edge Functions. All clients act as one
shared profile selected by DEMO_USER_ID. Run commands from the repository root.
Design and storage
The Edge Functions share one request handler and repository under
supabase/functions/social-feed.
| Data | Purpose |
|---|---|
profiles |
Bird identities, handles, and avatar URLs |
posts |
Author, text, optional image URL, and creation time |
likes |
One like per profile and post |
chirpy-avatars, chirpy-posts |
Public avatar and post images |
Database tables have row-level security enabled, with no direct anonymous or
authenticated access. The server uses service_role to call restricted database
functions and read the current profile's public fields. Search uses a trigram index
for case-insensitive substring matching.
Personal API key
All data routes require X-Chirpy-API-Key; health is public. This key grants access
to the shared demo, not an individual account. Generate a dedicated key with
openssl rand -hex 32 and use the same value for the backend's CHIRPY_API_KEY and
the app's ChirpyAPIKey (app setup).
Local env files and the app plist are Git-ignored. Keep them private, and never put the Supabase service-role key in the app. To rotate the personal key, update the backend secret, local env file, and app plist, then rebuild.
Run the backend locally
You need Docker, the Supabase CLI, and Deno.
supabase start
supabase db reset
cp supabase/functions/.env.example supabase/functions/.env
supabase status
db reset removes existing local data and loads the seed. For an existing database
whose data you want to keep, use supabase migration up --local instead.
Fill in supabase/functions/.env:
| Variable | Value |
|---|---|
SUPABASE_URL |
http://127.0.0.1:54321 |
SUPABASE_SERVICE_ROLE_KEY |
Local service-role key from supabase status |
CHIRPY_API_KEY |
Your dedicated personal key |
DEMO_USER_ID |
00000000-0000-4000-8000-000000000001 (Pip Sparrow) |
ENABLE_DEV_SCENARIOS |
true for local testing |
ALLOWED_ORIGIN |
Optional; leave blank for the iOS app |
supabase functions serve --env-file supabase/functions/.env --no-verify-jwt
Point the app at the local URL as described in Development.
With development scenarios enabled, feed requests accept scenario=slow, error,
empty, or duplicates.
API reference
Paths are relative to the Supabase base URL. Data routes also work under
/functions/v1/social-feed, such as /functions/v1/social-feed/feed.
| Method | Path | Result |
|---|---|---|
GET |
/functions/v1/social-feed/health |
Public { "status": "ok" } |
GET |
/functions/v1/feed |
Page of posts |
GET |
/functions/v1/profile |
Current profile |
GET |
/functions/v1/search |
Page of matching posts |
POST |
/functions/v1/posts |
New post; 201 |
DELETE |
/functions/v1/posts/{postID} |
Delete own post; 204, empty body |
POST |
/functions/v1/posts/{postID}/like |
Set a like |
DELETE |
/functions/v1/posts/{postID}/like |
Remove a like |
Feed and pagination
Feed and Search return { posts, nextCursor, hasMore }, newest first by
created_at DESC, id DESC. limit defaults to 20 and accepts 1–50.
Send nextCursor back unchanged as cursor; it is null at the end.
Posts contain id, author, text, imageURL, createdAt, isLiked, and likeCount.
Authors contain id, username, displayName, and avatarURL. Timestamps are UTC
ISO-8601 strings; image and avatar URLs may be null. No matches return an empty page.
Feed accepts an optional profile_id UUID to filter by author. Keep the same filter
when paginating. Like state always belongs to the shared demo profile.
Current profile
GET /functions/v1/profile returns id, username, displayName, and avatarURL
for DEMO_USER_ID, even when it has no posts. Use its id with the feed's profile_id
to fetch its posts.
Search
GET /functions/v1/search?q=crumb searches post text using case-insensitive literal
substrings. Names and handles are not searched; %, _, and backslashes are ordinary
characters, not wildcards.
Supply q once, with 2–100 Unicode code points after trimming and no null characters.
Keep the exact trimmed query, including case, when paginating. A changed query starts
without a cursor. Feed and Search cursors cannot be exchanged.
Create a post
Send a JSON object containing only text, with 1–300 Unicode code points after
trimming. Line breaks and internal whitespace are preserved. Image uploads and author
selectors are unsupported. Success returns a complete post; repeating a successful
request creates another post.
The shared profile allows five posts per rolling minute and 50 per rolling day. The database enforces these limits across concurrent requests. Deleting a recent post frees a slot in the applicable windows.
Delete a post
Supply a post UUID; no body is needed. Only the shared profile's own posts can be deleted.
Deletion permanently removes the post and its likes, but retains any Storage image.
Missing, already deleted, and other profiles' posts all return 404 post_not_found.
The app treats that response as already unavailable.
Likes
Supply a post UUID; no body is needed. Both methods return
{ postID, isLiked, likeCount }. Setting or removing a like is safe to repeat.
Errors
Errors return { "error": { "code", "message", "requestID" } } and an x-request-id header.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request |
Invalid input |
| 400 | invalid_cursor |
Malformed cursor or mismatched query/filter |
| 401 | unauthorized |
Missing or incorrect personal key |
| 404 | post_not_found |
Post missing or unavailable for the action |
| 404 | not_found |
Unknown route |
| 405 | method_not_allowed |
Unsupported method |
| 429 | rate_limit_exceeded |
Shared posting quota reached |
| 500 | internal_error |
Database or configuration failure |
| 503 | service_unavailable |
Development error scenario |
Try the API
Set CHIRPY_API_KEY in your shell to the configured personal key.
curl http://127.0.0.1:54321/functions/v1/social-feed/health
curl -G http://127.0.0.1:54321/functions/v1/search -H "X-Chirpy-API-Key: $CHIRPY_API_KEY" --data-urlencode 'q=crumb'
curl -X POST http://127.0.0.1:54321/functions/v1/posts -H 'Content-Type: application/json' -H "X-Chirpy-API-Key: $CHIRPY_API_KEY" -d '{"text":"Hello from Chirpy!"}'
Run the backend tests
deno task test
deno task check
deno fmt --check
deno lint
Handler tests use a fake repository. After applying local migrations, run the SQL checks to verify database behavior and permissions; each rolls back its test data.
supabase db query --local --file supabase/tests/demo_posting_limits.sql
supabase db query --local --file supabase/tests/search_posts.sql
supabase db query --local --file supabase/tests/feed_profile_filter.sql
supabase db query --local --file supabase/tests/current_profile_read.sql
supabase db query --local --file supabase/tests/delete_post.sql
Deploy the backend
Create a private production env file containing CHIRPY_API_KEY and DEMO_USER_ID.
Supabase supplies the hosted URL and service-role key; do not upload your local values.
Keep development scenarios disabled in production.
supabase login
supabase link --project-ref YOUR_PROJECT_REF
supabase db push
supabase secrets set --env-file path/to/production.env
supabase functions deploy social-feed feed posts search profile --no-verify-jwt
Apply migrations before deploying functions. JWT verification is disabled because
Chirpy checks its personal key in the handler. db push does not load seed.sql;
seed a fresh hosted demo separately if needed.
The bird demo
The seed contains 15 profiles, 120 posts, and eight post illustrations. Edit
supabase/demo/birds.json or the artwork manifests, then regenerate and validate:
deno task demo:build
deno task demo:check
For an existing demo, the publishing script uploads artwork and applies
supabase/demo/refresh-bird-demo.sql. The refresh updates known seeded records while
preserving timestamps, likes, and user-created posts; it does not insert missing records.
See the artwork guide for publishing steps and versioned image filenames.