Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
# OpenAI Moderation API — used by apps/api/src/services/moderation.ts
OPENAI_API_KEY=sk-...

# Postgres connection string for Drizzle (use the pooled Supabase URL)
SUPABASE_POOLER_DATABASE_URL=postgresql://user:pass@host:6543/postgres

# Supabase project — only needed if using Supabase-specific APIs beyond Postgres
SUPABASE_URL=
SUPABASE_SECRET_KEY=
# Cloudflare D1 / deploy tooling. The Worker receives D1 through the `DB`
# binding in apps/api/wrangler.jsonc; these values are only for Wrangler CLI
# commands such as migrations and deploys.
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_D1_DATABASE_ID=816d5f5d-0be0-4122-88a9-f4bcee86892e

# Clerk auth — API verifies session tokens through the Clerk backend SDK
CLERK_PUBLISHABLE_KEY=pk_test_...
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ jobs:
# - name: Check
# run: bun run check

- name: Apply D1 migrations
working-directory: apps/api
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: bun wrangler d1 migrations apply jematala-db --remote

# Cloudflare only accepts `wrangler secret put` after the Worker exists (initial deploy).
- name: Deploy API Worker
working-directory: apps/api
Expand All @@ -53,16 +60,10 @@ jobs:
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
SUPABASE_SECRET_KEY: ${{ secrets.SUPABASE_SECRET_KEY }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SUPABASE_POOLER_DATABASE_URL: ${{ secrets.SUPABASE_POOLER_DATABASE_URL }}
run: |
printf '%s' "$SUPABASE_POOLER_DATABASE_URL" | bun wrangler secret put SUPABASE_POOLER_DATABASE_URL
printf '%s' "$SUPABASE_URL" | bun wrangler secret put SUPABASE_URL
printf '%s' "$SUPABASE_SECRET_KEY" | bun wrangler secret put SUPABASE_SECRET_KEY
printf '%s' "$CLERK_PUBLISHABLE_KEY" | bun wrangler secret put CLERK_PUBLISHABLE_KEY
printf '%s' "$CLERK_SECRET_KEY" | bun wrangler secret put CLERK_SECRET_KEY
printf '%s' "$OPENAI_API_KEY" | bun wrangler secret put OPENAI_API_KEY
Expand Down
16 changes: 8 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ bun run typecheck:app # single-package typecheck
bun run build:app # expo export -p web
bun run build:api # wrangler deploy --dry-run

# DB reset (local only, needs .env with DATABASE_URL)
bun --cwd packages/db run reset:local
# DB reset (local only, needs .env with CLOUDFLARE_D1_DATABASE_ID)
bun run --cwd packages/db reset:local

# Deploy (after CI secrets configured — see README.md for list)
bun --cwd apps/api wrangler deploy
Expand All @@ -28,9 +28,9 @@ bun --cwd apps/app wrangler deploy # static SPA via CF Workers assets
## Architecture

- **Monorepo** (Bun workspaces): `apps/app` (Expo Router, entrypoint: `expo-router/entry`), `apps/api` (Hono on CF Workers), `packages/db` (SQL reset), `packages/shared` (Zod schemas + types)
- **No RLS** — DB logic lives in the Hono API worker. **Drizzle ORM planned but not yet installed.**
- **No RLS** — DB logic lives in the Hono API worker. Drizzle ORM is used for D1 schema/query helpers.
- **Auth**: Clerk (social login only — Google, Apple). No email/password.
- **Real-time**: Durable Objects manage WebSocket connections (planned).
- **Real-time**: Durable Objects manage WebSocket connections.
- **Two CF Workers**: `jematala-api` (`jematala.takuk.me/api/*`), `jematala-app` (`jematala.takuk.me/*`)
- **/events routes are scaffold placeholders** — PLAN.md says to replace them with map, billboard, studio, quests, profile routes.

Expand All @@ -41,11 +41,11 @@ bun --cwd apps/app wrangler deploy # static SPA via CF Workers assets
- Shared package subpath export: `@repo/shared/events`
- Path aliases in `tsconfig.base.json`: `@repo/db` → `packages/db/src`, `@repo/shared` → `packages/shared/src`
- Wrangler configs use `wrangler.jsonc` (not `.toml`), schema at `node_modules/wrangler/config-schema.json`
- DB uses **PostGIS** — `packages/db/supabase/reset.sql` creates the `app` schema and enables the extension
- DB uses **Cloudflare D1** — `packages/db/d1/schema.sql` creates local reset tables and seed data
- Shared package exports domain schemas via `@repo/shared` and subpaths such as `@repo/shared/poi`, `@repo/shared/billboard`, `@repo/shared/quest`, and `@repo/shared/user`
- Node 25, Bun latest (pinned via mise)
- `bun.lock` is checked in; CI uses `--frozen-lockfile`
- **Drizzle ORM** for Postgres queries (not Prisma)
- **Drizzle ORM** for D1 queries (not Prisma)
- **Maps**: `react-native-leaflet-view` + OSM
- **Icons**: `lucide-react-native`, `@expo/vector-icons`, and `expo-symbols` all available
- **HTTP client**: `@tanstack/react-query` for data fetching in the app
Expand All @@ -59,8 +59,8 @@ bun --cwd apps/app wrangler deploy # static SPA via CF Workers assets

- Expo web output is a static SPA; dynamic routes client-side via Expo Router.
- Node 25, Bun latest (pinned via mise). `bun.lock` checked in.
- **PostGIS** DB — `packages/db/supabase/reset.sql` creates the `app` schema and enables the extension.
- DB reset: source `.env` first (`set -a; source .env; set +a`) then `bun --cwd packages/db run reset:local`.
- **D1** DB — `packages/db/d1/schema.sql` creates local reset tables and seed data.
- DB reset: source `.env` first (`set -a; source .env; set +a`) then `bun run --cwd packages/db reset:local`.
- `wrangler types --env-interface CloudflareBindings` generates binding types for the API worker.
- Expo typed routes enabled (`app.json` experiments).
- `react-compiler` enabled in `app.json`.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ can track what's done. Use `- [x]` for completed items.
```
apps/app Expo Router app (web + mobile)
apps/api Cloudflare Worker API (Hono)
packages/db Supabase/PostGIS reset SQL
packages/db D1 reset SQL
packages/shared Shared Zod schemas & API contracts
```

Expand Down
18 changes: 9 additions & 9 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
## Phase 0 — Domain Alignment & Data Model (Everyone, together first)

- [x] **All 4** — Whiteboard the domain model: User, POI, Billboard, Placement (Sticker/StickyNote), Quest, DailyQuest, UserProgress, Report
- [x] **BE1** — Write `packages/db/supabase/reset.sql` with all real tables (POIs include picture column, users include avatar + is_admin columns)
- [x] **BE1** — Write `packages/db/d1/schema.sql` with all real tables (POIs include picture column, users include avatar + is_admin columns)
- [x] **BE1** — Write Drizzle schema in `packages/db/src/schema/` (POI: picture field; User: avatar field)
- [x] **BE1** — Write shared Zod schemas in `packages/shared/src/` (poi includes optional picture, user includes avatar)
- [x] **BE1** — Delete old `events`-related code from `packages/shared/src/events.ts`
Expand All @@ -29,7 +29,7 @@
## Phase 1a — Foundation: Backend

- [x] **BE2** — Set up Clerk JWKS verification middleware in `apps/api/src/middleware/auth.ts`
- [x] **BE2** — Set up Drizzle driver + Supabase connection in `apps/api/src/db.ts`
- [x] **BE2** — Set up Drizzle driver + D1 connection in `apps/api/src/db.ts`
- [x] **BE2** — User profile API: avatar upload (base64 PNG) + `PATCH /api/users/me/avatar`
- [x] **BE2** — Restructure `apps/api/src/index.ts` — split into route modules (`/pois`, `/billboards`, `/stickers`, `/quests`, `/users`, `/admin`)

Expand Down Expand Up @@ -62,7 +62,7 @@
- [x] **FE1** — Map: show POI markers with distinct glowing style
- [X] **FE1** — Map: show billboard markers with note icon style
- [x] **FE1** — Map: show user's current location as their 64×64 avatar (instead of a standard dot)
- [ ] **FE1** — POI discovery UX: toast when entering geofence + quest progress trigger (quest-progress toast plumbing exists for API mutations; geofence trigger still pending)
- [ ] **FE1** — POI discovery UX: toast when entering numeric radius check + quest progress trigger (quest-progress toast plumbing exists for API mutations; numeric radius check trigger still pending)
- [X] **FE2** — Billboard expanded view (~60vh overlay): text + username pill + all placements (z-ordered)
- [X] **FE2** — Pixel art sticker editor: 64×64 grid, 8-colour palette, tap-to-fill, save to collection
- [X] **FE2** — Sticky note composer: text input, preview as sticky note, post to billboard
Expand All @@ -73,7 +73,7 @@

- [x] **BE1** — Daily quest rotation logic + streak tracking
- [x] **BE1** — User profile API: `GET /api/users/:id`, `PATCH /api/users/me`
- [x] **BE2** — Durable Object: WebSocket handler, Postgres connection, broadcast on mutations
- [x] **BE2** — Durable Object: WebSocket handler, D1 connection, broadcast on mutations
- [ ] **BE2** — Expo Push Notification integration: register token, send on reply + daily reminder
- [x] **FE1** — Quest screen: main quest tiers + daily quest + streak counter + progress bars (currently backed by mock quest data)
- [x] **FE1** — Profile screen: level, perks unlocked, stats (notes placed, stickers saved, POIs visited)
Expand Down Expand Up @@ -121,16 +121,16 @@ Phase 1b BE ──► Phase 4 BE (reporting, analytics)
## Key Architectural Decisions (confirmed)

- **Sticker storage:** base64 PNG blob — FE produces B64 string, sends to BE for moderation (B64 moderation via OpenAI)
- **Admin role:** `is_admin` boolean column on `app.users`
- **Primary keys:** internal UUIDv4 values for all primary keys; Clerk user IDs are stored as unique external auth identifiers on `app.users.clerk_user_id`
- **Admin role:** `is_admin` boolean column on `users`
- **Primary keys:** internal UUIDv4 values for all primary keys; Clerk user IDs are stored as unique external auth identifiers on `users.clerk_user_id`
- **Map on mobile:** `react-native-leaflet-view` (pavel-corsaghin/react-native-leaflet)
- **Drizzle migrations:** Drizzle Kit with `drizzle-kit push` for hackathon speed
- **D1 migrations:** SQL files under `packages/db/d1/migrations`; local resets use `packages/db/d1/schema.sql`
- **Billboard limits:** concurrent active cap starts at 3 and scales with level; posting at the cap soft-deletes the user's oldest active billboard before publishing the new one
- **Billboard daily limit:** separate Sydney calendar-day posting cap; seeded as concurrent + 1 and capped at 10/day
- **Billboard expiry:** derived from `empty_expires_at`/`expires_at` in active queries (empty billboards disappear after 24 hours; all billboards disappear after 5 days)
- **Daily quests:** seeded templates/pool; active quest is deterministically selected from the Sydney calendar day
- **POI rotation:** seeded/admin-created POI table; active POI set is deterministically selected from the campus calendar day
- **POI geofence radius:** 30m
- **POI numeric radius check radius:** 30m
- **Quest system:** parameterised templates (visit N POIs, leave N notes, place N stickers, receive N replies, save N stickers) with per-level randomised values and tier progression
- **Daily quest pool:** 5 curated seeded daily quests; one rotates each Sydney calendar day
- **Push notification timing:** 8–9am daily quest reminder when push is enabled
Expand Down Expand Up @@ -210,4 +210,4 @@ Hardcoded in `constants/coordinates.ts`:

### Maps Backlog (post-hackathon)

- POI discovery toast on geofence enter
- POI discovery toast on numeric radius check enter
18 changes: 9 additions & 9 deletions PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A location-based social exploration game for UNSW students.

## 1. Overview

Mobile app where students discover geofenced Points of Interest (POIs) around UNSW Kensington campus, leave billboard notes, and reply with pixel-art stickers. Progression via quests and levelling unlocks cosmetic perks, billboard capacity, and sticker collection capacity upgrades.
Mobile app where students discover radius-based Points of Interest (POIs) around UNSW Kensington campus, leave billboard notes, and reply with pixel-art stickers. Progression via quests and levelling unlocks cosmetic perks, billboard capacity, and sticker collection capacity upgrades.

**Core loop:** Explore campus → discover POIs → leave/read notes → reply with stickers → complete quests → level up → unlock perks → explore more.

Expand All @@ -16,10 +16,10 @@ Mobile app where students discover geofenced Points of Interest (POIs) around UN

| Layer | Choice |
| --------------------------- | ------------------------------------------------------------------------- |
| Mobile framework | Expo (React Native) with native geofencing |
| Mobile framework | Expo (React Native) with native numeric radius checks |
| API runtime (non-real-time) | Hono on Cloudflare Workers |
| Real-time runtime | Cloudflare Durable Objects (WebSockets, broadcasting) |
| Database | PostgreSQL + PostGIS on Supabase (no RLS) |
| Database | Cloudflare D1 (no RLS) |
| ORM | Drizzle |
| Auth | Clerk (social login only — Google, Apple) |
| Maps | react-native-leaflet-view + OSM |
Expand All @@ -36,10 +36,10 @@ Cloudflare Workers are the entry point for all HTTP requests. Routing depends on

| Request type | Handler | Example |
| ------------------ | ----------------------------------------- | --------------------------------------- |
| Non-real-time GET | CF Worker (Hono) → Postgres | Fetch user profile, list saved stickers |
| Non-real-time POST | CF Worker (Hono) → Postgres | Update user display name, settings |
| Real-time GET | Durable Object → Postgres (via WebSocket) | Query notes, POIs, and map data |
| Real-time POST | Durable Object → Postgres + broadcast | Post a note, place a sticker |
| Non-real-time GET | CF Worker (Hono) → D1 | Fetch user profile, list saved stickers |
| Non-real-time POST | CF Worker (Hono) → D1 | Update user display name, settings |
| Real-time GET | Durable Object → D1 (via WebSocket) | Query notes, POIs, and map data |
| Real-time POST | Durable Object → D1 + broadcast | Post a note, place a sticker |
| WebSocket connect | Durable Object (persistent connection) | Live map updates, push notifications |

Durable Objects manage persistent WebSocket connections for real-time features — querying notes and map data, broadcasting changes, and sending notifications. Mutations required for real-time functionality (e.g. posting notes, placing stickers) also run inside the Durable Object so the result can be broadcast immediately.
Expand All @@ -53,7 +53,7 @@ Non-real-time requests (e.g. updating user settings, fetching saved stickers) go
- 2D top-down map centered on UNSW Kensington campus
- User sees: their own location dot, POI markers, and billboard notes
- **No other users are visible** on the map — anonymity of presence
- POI geofence radius: 30m (tuned during playtesting if needed)
- POI numeric radius check radius: 30m (tuned during playtesting if needed)
- Map provider: react-native-leaflet-view with OSM tiles
- User represented on map by a 64×64 pixel art avatar drawn at sign-up

Expand Down Expand Up @@ -223,7 +223,7 @@ Quests are **parameterised templates** rather than fixed one-time objectives. Ea
| -------------------------------- | -------------------------------------- |
| Primary key format | **UUIDv4** for internal primary keys; Clerk IDs are unique auth-provider identifiers, not primary keys |
| Billboard limits | **Concurrent cap + Sydney-day posting cap**; posting at the concurrent cap replaces the user's oldest active billboard |
| POI geofence trigger radius | **30m** |
| POI numeric radius check trigger radius | **30m** |
| Map provider | **react-native-leaflet-view** + OSM |
| Sticker storage format | **base64 PNG** |
| Quest system | **Parameterised templates** (visit N POIs, leave N notes, place N stickers, receive N replies, save N stickers) with per-level randomised values |
Expand Down
Loading
Loading