Skip A Snack

Master Technical Reference
Updated: 21 March 2026  ·  Status: Current

Contents

  1. Database Schema
  2. Data Flow
  3. API Endpoints
  4. File Structure
  5. Issues, Notes & Resolved
1 Database Schema

PostgreSQL via Drizzle ORM. Connection string: DATABASE_URL environment variable. Internal hostname: helium. All tables live in the public schema.

users  illustrative

The core identity table. One row per registered user.

ColumnDB TypeDrizzle TypeNullableDefault
idcharacter varyingvarchar · PKNOgen_random_uuid()
emailtexttext · UNIQUENO
first_namecharacter varyingvarcharYES
last_namecharacter varyingvarcharYES
created_attimestamptimestampNOCURRENT_TIMESTAMP
countrytexttextYES
citytexttextYES
latitudedouble precisiondoublePrecisionYES
longitudedouble precisiondoublePrecisionYES
marketing_consentbooleanbooleanYESfalse
member_numberintegerintegerYESauto-assigned on registration
founding_interest_attimestamptimestampYES

magic_codes  illustrative

One-time 6-digit sign-in codes sent by email. Codes expire after 10 minutes and are single-use.

ColumnDB TypeDrizzle TypeNullableDefault
idintegerserial · PKNOauto-increment
emailtexttextNO
codetexttextNO
expires_attimestamptimestampNO
usedbooleanbooleanNOfalse
created_attimestamptimestampNOCURRENT_TIMESTAMP

shared_skips  illustrative

A record is created each time a user taps "Share" on a skip result. Used to generate public share URLs and the kudos page. photo_data stores a full base64-encoded JPEG inline.

ColumnDB TypeDrizzle TypeNullableDefault
idcharacter varyingvarchar · PKNOgen_random_uuid()
food_nametexttextNO
descriptiontexttextNO
calories_savedintegerintegerNO
walking_minutesintegerintegerNO
photo_uritexttextNO
photo_datatexttextYES
user_emailtexttextYES
kudos_countintegerintegerNO0
created_attimestamptimestampNOCURRENT_TIMESTAMP

kudos  illustrative

One row per unique kudos given. Deduplication is by SHA-256 hash of (giver_ip + shared_skip_id) — one IP can only give kudos once per shared skip.

ColumnDB TypeDrizzle TypeNullableDefault
idintegerserial · PKNOauto-increment
shared_skip_idcharacter varyingvarchar · FK → shared_skips.idNO
giver_hashtexttextNO
giver_nametexttextYES
created_attimestamptimestampNOCURRENT_TIMESTAMP

notifications  illustrative

In-app notification inbox. Populated server-side whenever someone gives kudos on a shared skip that has a user_email attached.

ColumnDB TypeDrizzle TypeNullableDefault
idintegerserial · PKNOauto-increment
user_emailtexttextNO
typetexttextNO
titletexttextNO
bodytexttextNO
shared_skip_idcharacter varyingvarcharYES
readbooleanbooleanNOfalse
created_attimestamptimestampNOCURRENT_TIMESTAMP

user_sync  illustrative

One row per user, overwritten on every sync. Stores the user's complete current app state as JSONB blobs. This is the server-side backup of local AsyncStorage. user_id FK is ON DELETE CASCADE.

ColumnDB TypeDrizzle TypeNullableDefault
idintegerserial · PKNOauto-increment
user_idcharacter varyingvarchar · UNIQUE · FK → users.id (CASCADE)NO
skipsjsonbjsonbNO[]
treatsjsonbjsonbNO[]
streakjsonbjsonbNO{}
updated_attimestamptimestampNOnow()
2 Data Flow

Primary data store: Device (AsyncStorage)

The app's skip history, treat history, and streak data live on the user's device in React Native AsyncStorage. The server is a secondary backup, not the source of truth. All in-app screens read from AsyncStorage via lib/storage.ts. The server only receives data when the app explicitly syncs.

AsyncStorage Keys (all devices)

KeyContent
skip_a_snack_skipsArray of Skip objects — full skip history
skip_a_snack_treatsArray of Treat objects — redeemed treats
skip_a_snack_user{ email, userId } — logged-in user identity
skip_a_snack_streak{ currentStreak, longestStreak, lastSkipDate, seenMilestones }
skip_a_snack_scans:${date}{ skips: n, treats: n } — daily scan rate-limiter (max 5 snaps/day)

Keys migrated from legacy didnt_eat_that_ prefix via one-time startup migration (sas_migrated_v1 flag). Migration runs in app/_layout.tsx before any context providers initialise.

users

DirectionActorDetails
WritePOST /api/auth/verify-codeCreates user on first successful login
WritePUT /api/user/profileUpdates first_name, last_name, marketing_consent
WritegeolocateAndStore()Updates country/city/lat/lng after first login via ip-api.com — fire-and-forget
WriteDELETE /api/user/accountDeletes the row
ReadGET /api/user/profileReturns email, created_at, first/last name
ReadGET /api/stats/worldReturns lat/lng pins and country aggregates
ReadAll auth-gated endpointsgetUserByEmail() validates caller identity

magic_codes

DirectionActorDetails
WritePOST /api/auth/send-codeInserts a new code; sends email via Resend
WriteverifyMagicCode()Marks used code as used = true
WriteDELETE /api/user/accountDeletes all codes for the email
ReadverifyMagicCode()Looks up unused, unexpired code matching email + code

shared_skips

DirectionActorDetails
WritePOST /api/skips/shareCreates a row when user shares a skip
WritePATCH /api/skips/:id/photoUpdates photo_data after sharing
WritePOST /api/skips/:id/kudosIncrements kudos_count atomically
ReadGET /api/skips/:idReturns skip metadata for the share card
ReadGET /api/skips/:id/imageReturns raw JPEG from photo_data
ReadGET /shared/:id (HTML)Renders the kudos page
ReadPOST /api/skips/:id/kudosLooks up user_email to create notification

kudos

DirectionActorDetails
WritePOST /api/skips/:id/kudosInserts a row if the giver hash hasn't already voted
ReadaddKudos()Checks for existing kudos by (shared_skip_id, giver_hash)

notifications

DirectionActorDetails
WritePOST /api/skips/:id/kudosCreates a notification for the skip owner
ReadPOST /api/notificationsReturns up to 50 most recent notifications
WritePOST /api/notifications/readMarks all user's notifications as read
WriteDELETE /api/user/accountDeletes all notifications for the email

user_sync

DirectionActorDetails
WritePOST /api/user/syncUpserts entire current state (skips, treats, streak) from device — overwrites previous snapshot
ReadGET /api/user/restoreReturns stored JSON snapshot on login or device restore
3 API Endpoints

All routes served by the Express server on port 5000. No authentication middleware — identity is passed as email or userId in the request body or query string.

Authentication

MethodPathPurposeTables Touched
POST /api/auth/send-code Generates a 6-digit code, stores it in DB, sends email via Resend. Code expires after 10 minutes. magic_codes W
POST /api/auth/verify-code Validates code, marks it used, creates user if first login, fires geolocation lookup (async). magic_codes R/W · users R/W

Food Analysis

MethodPathPurposeTables Touched
POST /api/analyze-food Receives base64 image, sends to Claude claude-sonnet-4-6 via Anthropic API. Returns food name, description, calories, walking minutes, portion note, confidence. None

Skips

MethodPathPurposeTables Touched
POST /api/skips/share Creates a shared skip record, returns shareUrl built from SHARE_BASE_URL env var or request host. shared_skips W
POST /api/skips/upload-photo Uploads photo to R2 and returns photoUrl. Called after every successful skip. Previously named /api/skips/log — DB write removed, R2 upload retained. None (R2 object storage)
GET /api/skips/:id Returns skip metadata (no photo binary) for the share card UI. shared_skips R
PATCH /api/skips/:id/photo Updates the base64 JPEG on an existing shared skip. shared_skips W
GET /api/skips/:id/image Returns the raw JPEG binary from photo_data, with long-lived cache headers. shared_skips R
POST /api/skips/:id/kudos Adds kudos (deduped by IP hash), increments counter on shared_skips, creates notification for skip owner. kudos R/W · shared_skips W · notifications W

Notifications

MethodPathPurposeTables Touched
POST /api/notifications Returns up to 50 notifications + unread count for a user. Email passed in body. Semantically a read despite using POST. users R · notifications R
POST /api/notifications/read Marks all unread notifications as read for a user. users R · notifications W

User Profile & Data

MethodPathPurposeTables Touched
GET /api/user/profile Returns email, created_at, first_name, last_name. Email passed as query param. users R
PUT /api/user/profile Updates first_name, last_name, and optionally marketing_consent. users W
DELETE /api/user/account Deletes user row plus their notifications and magic codes. Does not delete shared_skips, kudos, or skips_log rows. users D · notifications D · magic_codes D
POST /api/user/sync Upserts entire local state (skips, treats, streak JSON) from device. Overwrites previous snapshot. user_sync W
GET /api/user/restore Returns stored sync snapshot by userId. Used on login / device restore. user_sync R

Stats

MethodPathPurposeTables Touched
GET /api/stats/world Returns lat/lng pins, country aggregates (name + count), total user count, and country count. Used by the world map on the Journey tab. users R

Server-rendered HTML

MethodPathPurposeTables Touched
GET /shared/:id Renders the public kudos page for a shared skip using server/templates/kudos-page.html. Populated from shared_skips. shared_skips R
4 File Structure
/
├── app/                           Expo Router screens (mobile + web)
│   ├── _layout.tsx                Root layout: fonts, providers, auth gate
│   ├── auth.tsx                   Email + 6-digit code sign-in screen
│   ├── onboarding.tsx             4-screen first-run onboarding flow
│   ├── capture.tsx                Camera / photo capture screen
│   ├── result.tsx                 Post-capture result: food ID + calories
│   ├── skip-detail.tsx            Detail view for a single past skip
│   ├── skip-limit.tsx             Daily scan limit reached screen
│   ├── treat-history.tsx          Full history of redeemed treats
│   ├── notifications.tsx          In-app notification inbox
│   ├── +not-found.tsx             404 fallback
│   ├── +native-intent.tsx         Deep link intent handler
│   └── (tabs)/
│       ├── _layout.tsx            Tab bar layout (Home, Journey, Profile)
│       ├── index.tsx              Home tab: today's skips, calorie counter, treats
│       ├── journey.tsx            Journey tab: skip timeline, world map, stats
│       └── profile.tsx            Profile tab: settings, notifications, account
│
├── components/
│   ├── ErrorBoundary.tsx          React error boundary (class component)
│   ├── ErrorFallback.tsx          Error fallback UI
│   ├── KeyboardAwareScrollViewCompat.tsx
│   ├── WorldMap.tsx               Web stub (no-op — maps not rendered on web)
│   └── WorldMap.native.tsx        Native world map — react-native-maps@1.18.0
│
├── constants/
│   └── colors.ts                  Design token colours (coral, cream, terracotta…)
│
├── lib/
│   ├── auth-context.tsx           Auth state: login, logout, session restore
│   ├── skip-context.tsx           Global skip/treat state + server sync trigger
│   ├── storage.ts                 ALL local AsyncStorage reads/writes
│   │                              (skips, treats, streak, user, scan counts)
│   ├── theme-context.tsx          Day/night theme toggle
│   ├── notification-context.tsx   Unread notification badge state
│   └── query-client.ts            React Query client + apiRequest() + getApiUrl()
│
├── server/
│   ├── index.ts                   Express setup: CORS, body parsing, Expo
│   │                              manifest routing, kudos page, SPA catch-all
│   ├── routes.ts                  All /api/* route handlers
│   ├── storage.ts                 DatabaseStorage class — all DB R/W via Drizzle
│   ├── db.ts                      Drizzle + pg pool init from DATABASE_URL
│   └── templates/
│       ├── landing-page.html      Expo Go QR landing page for browser visitors
│       └── kudos-page.html        Public kudos share page (/shared/:id)
│
├── shared/
│   └── schema.ts                  Drizzle table definitions, Zod schemas, TS types
│
├── public/
│   └── (empty — marketing pages removed to separate website project)
│
├── scripts/
│   ├── build.js                   Production build (expo export + tsc)
│   └── start-frontend.sh          Starts Expo dev server with env vars injected
│
├── app.json                       Expo config: bundle ID, splash, plugins
├── drizzle.config.ts              Drizzle Kit config (db:push / migrations)
├── package.json                   Dependencies and npm scripts
└── TECHNICAL_REFERENCE.md        Source document for this file

Key npm scripts

ScriptWhat it does
npm run server:devStarts Express in dev mode via tsx (hot reload)
npm run expo:devStarts Expo Metro bundler for development
npm run db:pushSyncs Drizzle schema to the database safely (no destructive migration)
npm run server:buildCompiles Express server TypeScript to JS
npm run expo:static:buildExports Expo web build to dist/
npm run server:prodRuns the compiled server in production
5 Issues, Notes & Resolved

Outstanding — To Do

Notes & By Design

Resolved

Skip A Snack — Master Technical Reference Updated 21 March 2026