Skip to content

Sooke Community App -- Project Plan

This document is the source of truth for the current state of the Sooke Community App project.


A mobile community app for Sooke, BC, Canada — a small coastal town with no existing local community app. The app serves Sooke residents and visitors with local business listings, restaurant menus, community events, and a map of local points of interest.

This is a personal project with no monetization goal. The developer is building and maintaining it solo, at least initially.


  • Give Sooke residents a single place to discover local events, restaurants, and businesses.
  • Allow verified business owners to manage their own listings and menus.
  • Provide a map view of local businesses and event locations.
  • Work well on both iOS and Android.
  • Support searching and filtering for businesses and events from day one.

  • iOS: SwiftUI (Swift 6.0, iOS 18.0+). Native app built with Xcode and XcodeGen.
  • Android (future): Kotlin + Jetpack Compose. Ported from the iOS app as a blueprint.
  • Architecture: MVVM with @Observable ViewModels, SwiftUI views, and an API service layer.
  • See: ADR-013 for why we chose native over Capacitor (supersedes ADR-001).
  • Language: Go
  • Framework: Chi — lightweight, idiomatic, built on net/http.
  • Role: REST API serving events, business listings, menus. Handles auth token validation.
  • See: ADR-003 for why we chose Chi over Fiber.
  • Local dev: PostgreSQL on the developer’s NAS. No Docker required.
  • Production: Railway managed PostgreSQL (same schema, environment variable swap only).
  • See: ADR-008 for why we chose NAS-hosted Postgres over Docker for local development.
  • Schema: businesses, menus, menu_items, events, event_types, business_categories, users, roles, business_hours, device_tokens.
  • IDs: Every public-facing entity has both a numeric primary key and a unique slug (e.g., joes-coffee-shop). Slugs are used in API responses and prepared for future deep linking.
  • Provider: Firebase Auth — native SDKs for iOS and Android.
  • Social login: Google, Apple (required by App Store), Facebook. Available to all users.
  • JWT validation: Go backend validates Firebase JWTs using firebase-admin-go in Chi middleware on every protected route.
  • See: ADR-014 for why we chose Firebase Auth over Clerk (supersedes ADR-002).
  • Library: MapLibre Native iOS SDK — open-source native map renderer.
  • Tile provider: MapTiler free tier (100k tile loads/month).
  • Usage: Pin businesses and event locations on a map of Sooke.
  • See: ADR-004 for why we chose MapLibre over Google Maps.
  • Provider: Cloudflare R2 — S3-compatible, no egress fees.
  • Usage: Business logos, photos, and any uploaded media. Store the URL in Postgres, serve from R2.
  • See: ADR-005 for why we chose R2 over S3.
  • Platform: Railway
  • Services: Go API container + managed PostgreSQL instance.
  • Config: All connection strings and secrets via environment variables.
  • Framework: SvelteKit (separate web app from the mobile app).
  • Hosting: Cloudflare Pages.
  • Role: CRUD operations for businesses, events, users, and tags. Super Admin access only.
  • Framework: Starlight (Astro-based documentation site).
  • Hosting: Cloudflare Pages.
  • See: ADR-007 for why we chose Starlight.

RoleHow they authenticateCapabilities
Anonymous visitorNo account neededBrowse businesses, menus, events, map. Read-only.
General userSocial login (Google/Apple/Facebook)Everything anonymous can do + submit events for review + subscribe to notifications.
Business ownerSocial login + manually promoted by Super AdminEverything general user can do + edit their own business listing, menus, hours. Approve or reject events at their venue.
Super Admin (developer)Social login + hardcoded roleEverything. Add/remove businesses, promote users, approve/reject any event, manage all content.
  1. The Super Admin creates the business listing. No user can self-register a business.
  2. A real business owner contacts the Super Admin out-of-band (email, in-person, phone).
  3. The Super Admin verifies their identity and business ownership.
  4. The business owner creates an account using social login (same as any other user).
  5. The Super Admin promotes their account to “business owner” scoped to their specific business via the admin dashboard. This links their user ID to the business ID in the database.

Social login provides identity verification. Manual promotion provides business ownership verification. These are two separate concerns handled by two separate mechanisms.

The app is fully usable without logging in. Users only need an account to submit events, receive notifications, or manage a business. Most users will never create an account.


Any logged-in user can submit an event. Events require approval before they are visible.

When creating an event, the submitter chooses one of two location types:

  • “At a business” — select a business from a dropdown. This creates a business_id foreign key on the event. The business owner can approve or reject it. If the submitter is the business owner, the event location auto-fills from the business’s stored coordinates.
  • “Public location” — drop a pin on the map or type an address. No business association. Only the Super Admin can approve.
  1. User submits an event. Status: pending_review.
  2. If the event is at a business, the business owner is notified (in-app + email) and can approve or reject.
  3. If the event is at a public location, the Super Admin reviews and approves or rejects.
  4. Approved events are visible to everyone.
  5. Rejected events notify the submitter with an optional reason. The submitter can resubmit at a different location if appropriate.

draft, pending_review, approved, rejected

  • Businesses are only created by the Super Admin. No self-registration.
  • Business owners are manually verified and promoted.
  • Event submissions require a logged-in account (social login adds friction).
  • Rate limiting on event submissions (e.g., max 5 pending events per user).
  • Only approved events are visible in the public feed.

Both businesses and events use a curated tag system. Tags are managed by the Super Admin. Users select from the predefined list and cannot create custom tags.

Business categories (examples): Restaurant, Cafe, Bar, Retail, Grocery, Outdoor Recreation, Health and Wellness, Arts and Culture, Professional Services, Accommodation.

Event types (examples): Live Music, Market, Workshop, Community Meeting, Sports, Festival, Fundraiser, Kids and Family, Outdoor.

If a user or business owner needs a tag that does not exist, they can request it. The Super Admin reviews and adds it to the master list if appropriate.

See ADR-006 for why we chose curated tags over free-form.


We test every layer of the application. The testing pyramid guides our approach: many unit tests, fewer integration tests, fewer E2E tests.

LayerWhat it testsTools
Unit tests (Go)Individual functions, service logic, validationGo standard testing package, table-driven tests
Unit tests (Swift)ViewModels, models, services, themeSwift Testing framework (@Suite, @Test)
Integration tests (API)HTTP handlers with real DB, middleware chainsGo testing + httptest + test Postgres container
Integration tests (DB)Migrations, queries, constraints, foreign keysGo testing + test Postgres container
API contract testsResponse shapes, status codes, error formatsGo testing or Hurl

Every milestone issue specifies which test layers are required. No issue is complete until its tests pass.


  • Browse local businesses (restaurants, shops, services)
  • Search and filter businesses by name and category
  • View restaurant menus
  • Browse upcoming community events
  • Search and filter events by type
  • Map view with pins for businesses and events
  • Submit community events for review (requires account)
  • Edit their own business listing (name, description, hours, contact, location)
  • Add, edit, and remove menu items and prices
  • Approve or reject events submitted at their venue
  • Create events at their own venue (auto-fills location)
  • Add new businesses to the directory
  • Create and manage community events
  • Verify and promote business owner accounts
  • Manage curated tag lists (business categories, event types)
  • Admin dashboard (separate SvelteKit web app)

Each milestone is tracked on GitHub. Each milestone contains one or more consolidated issues. Every issue includes tests in its acceptance criteria — nothing is complete until tests pass.

Issues link: github.com/KiefBC/sooke-community-app/issues

  • Initialize Capacitor + SvelteKit project
  • Verify app builds and runs on iOS simulator
  • Verify app builds and runs on Android emulator
  • Scaffold Go + Chi API with health check endpoint
  • Write test for health check endpoint
  • Set up PostgreSQL connection (NAS-hosted — see ADR-008)
  • Verify API connects to Postgres
  • Set up .env config pattern for local development
  • Write integration test for DB connection
  • Set up Starlight documentation site and deploy to Cloudflare Pages

Set up the Postgres schema, migrations, and seed data that everything else builds on.

  • Database schema, migrations, and seed data (#107) — schema design, slug fields, lat/lng coordinates, reversible migrations with Goose, constraint tests, and sample Sooke business seed data

Stand up the business listings API and frontend so users can browse Sooke businesses.

  • Business listings API (#108) — GET /api/v1/businesses (list with search/filter), GET /api/v1/businesses/:slug (detail), unit/integration tests, contract tests
  • Business list and detail UI (#109) — Svelte list and detail components, loading/error states, Vitest component tests
  • Business category filtering (#110) — GET /api/v1/categories endpoint, category filter on business list, filter UI component, tests for both API and UI

Integrate MapLibre GL JS and place business pins on a map of Sooke.

  • MapLibre integration with business pins (#111) — MapLibre + MapTiler setup, business pins at lat/lng, clickable popups linking to detail, E2E tests, iOS/Android webview verification

Build out the events system — API, frontend, map pins, and the event-business location link.

  • Events API and filtering (#112) — GET /api/v1/events (list with search/filter), GET /api/v1/events/:slug (detail), event type filtering, unit/integration/contract tests
  • Event list, detail, and map pins UI (#113) — Svelte event components, event pins on map (distinct from business pins), Vitest component tests
  • Event-business location association (#114) — foreign key relationship, event form with “At a business” / “Public location” toggle, auto-fill coordinates from business, association tests

Integrate Clerk for social login on the frontend and JWT validation on the backend.

  • Clerk auth integration (#115) — Clerk account setup, SvelteKit integration, Go Chi JWT middleware, protect write endpoints, middleware tests (valid/expired/missing token), iOS/Android login verification

Implement the role system that controls who can do what.

  • Role-based access control (#116) — role model in DB (super_admin, business_owner, general_user), role-checking middleware, scoped business owner permissions, user promotion endpoint, RBAC tests

Give business owners control of their listings and build the event approval workflow.

  • Business owner editing UI (#117) — edit form for business listing, menu management UI, business hours editing, E2E tests, permission boundary tests
  • Event submission and approval workflow (#118) — POST /api/v1/events, approve/reject endpoints, rate limiting, submission form, approval queue UI, notifications, full-flow tests

Build the Super Admin dashboard as a separate SvelteKit app on Cloudflare Pages.

  • Admin dashboard scaffolding (#119) — SvelteKit project init, Cloudflare Pages deploy, Super Admin auth check, navigation and layout
  • Admin dashboard CRUD pages (#120) — business management, event management, user management, tag management, CRUD tests

Add image upload support backed by Cloudflare R2.

  • Image upload with Cloudflare R2 (#121) — R2 bucket setup, upload endpoint (jpg/png/webp, max 5MB), store URLs in Postgres, upload UI with drag-and-drop, upload tests

Add full-text search and lock down API versioning.

  • Full-text search and API versioning (#122) — Postgres full-text search for businesses and events, route audit for /api/v1/ prefix, versioning docs, search tests

Containerize the API and deploy everything to Railway.

  • Dockerize and deploy to Railway (#123) — multi-stage Dockerfile, Railway deploy, migrate Postgres to Railway, update app to production API URL, end-to-end production verification

Polish, test on real devices, and submit to app stores.

  • Production polish and device testing (#124) — UI polish and loading states, real iOS device testing, real Android device testing, performance testing on lower-end Android, platform-specific fixes
  • App store submission (#125) — app store listings and screenshots, Terms of Service and Privacy Policy, Google Play submission, Apple App Store submission

  • Use .env files for local development.
  • Use Railway environment variables for production.
  • Never commit secrets to version control.
  • Never hardcode connection strings, API keys, or Clerk secrets.

ToolDetail
Primary dev machineMacBook Pro M4 Pro 48GB
iOS buildsOn MBP (Xcode required)
Android testingDeveloper’s personal Android phone
Windows PCAvailable for cross-platform testing
LanguagesGo, TypeScript, Svelte, some Rust
Prior Capacitor experienceNone (new for this project)
Prior SvelteKit experienceFamiliar with Svelte, some SvelteKit
Prior Railway experienceBasic familiarity

These are hard constraints for development. Do not deviate from these without explicit discussion and a new ADR.

  • Do not scaffold anything not listed in the current milestone.
  • Complete the current milestone before starting the next.
  • Every issue must include tests before it is considered complete.
  • Go tests use the table-driven pattern.
  • Always use environment variables for DB connections, API keys, and Clerk secrets. Never hardcode.
  • Postgres is the only server-side data store. Do not introduce other stores without discussion.
  • Business logic lives in Go, not in the frontend.
  • Svelte components should be small and composable.
  • MapLibre is used via Typescript in the Capacitor webview. Do not use a native maps plugin.
  • Clerk JWT validation happens in Chi middleware on every protected route.
  • Businesses are only created by the Super Admin. No self-registration.
  • No free-form tags at MVP. Use the curated tag list managed by Super Admin.
  • Chi for HTTP routing. Never Fiber.
  • API routes are prefixed with /api/v1/.
  • All documentation follows the rules in style-guide.md.