Skip to content

Backend

Samuel Yanez edited this page Oct 11, 2026 · 8 revisions

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.