Repository navigation
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.
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.
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.
You need Docker, the Supabase CLI, and Deno.
supabase start
supabase db reset
cp supabase/functions/.env.example supabase/functions/.env
supabase statusdb 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-jwtPoint the app at the local URL as described in Development.
With development scenarios enabled, feed requests accept scenario=slow, error,
empty, or duplicates.
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 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.
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.
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.
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.
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.
Supply a post UUID; no body is needed. Both methods return
{ postID, isLiked, likeCount }. Setting or removing a like is safe to repeat.
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 |
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!"}'deno task test
deno task check
deno fmt --check
deno lintHandler 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.sqlCreate 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-jwtApply 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 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:checkFor 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.