Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and r

- [Architecture overview](docs/architecture.md)
- [Build configuration (Supabase + Bugsnag)](docs/buildconfig-setup.md)
- [Local Supabase development (test env instead of prod)](docs/supabase-local-development.md)
- [Developer settings (debug-only env switch + test-user login)](docs/developer-settings.md)
- [Deployment (Android / iOS / Desktop)](docs/deployment.md)
- [Deep linking setup](docs/DEEP_LINKING_SETUP.md)
Expand Down
12 changes: 12 additions & 0 deletions client/composeApp/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Debug-only manifest overlay. Merged into the main manifest for debug builds only, so the
cleartext allowance below never reaches a release build. Lets the emulator talk to the
local Supabase stack over HTTP. See docs/supabase-local-development.md.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:networkSecurityConfig" />
</manifest>
19 changes: 19 additions & 0 deletions client/composeApp/src/debug/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Debug-only network security config. Permits cleartext (HTTP) traffic to the local
Supabase stack so the Android emulator can reach it during development.

- 10.0.2.2 is the host loopback as seen from the Android emulator (i.e. your Mac's
localhost where `supabase start` runs).
- localhost / 127.0.0.1 cover running against a device-local proxy.

This file lives in the `debug` source set ONLY, so release builds keep Android's default
(cleartext blocked). See docs/supabase-local-development.md.
-->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>
1 change: 1 addition & 0 deletions docs/buildconfig-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,5 @@ buildkonfig {

## Related

- [supabase-local-development.md](supabase-local-development.md) — run a full local Supabase stack and point `supabase.testing.*` at it so you develop against a test backend instead of prod.
- [developer-settings.md](developer-settings.md) — how to wire up the staging Supabase URL (`supabase.testing.*` / `SUPABASE_TESTING_*`) and pre-baked test users for the in-app developer menu.
2 changes: 2 additions & 0 deletions docs/developer-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ A debug-only screen reachable from the bottom of the **More** tab that lets you

The Supabase URL/key for **TESTING** is read at build time. PROD already has its values (see [buildconfig-setup.md](buildconfig-setup.md) for the existing `supabase.url` / `supabase.key`). FAKE re-uses PROD.

> **Recommended TESTING backend:** a **local Supabase stack** (Docker) rather than a hosted staging project — it's free, isolated from prod, and works offline. See [supabase-local-development.md](supabase-local-development.md) for the full setup, including per-platform networking (`localhost` vs `10.0.2.2`) and seeded test users. The values below apply to either a local stack or a hosted staging project.

### `local.properties`

```properties
Expand Down
265 changes: 265 additions & 0 deletions docs/supabase-local-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
# Local Supabase Development

Develop against a **fully local Supabase stack** (Postgres + Auth + Storage + Edge Functions +
Studio, all in Docker) instead of testing against the production project. The app already has a
`TESTING` environment baked in — this guide stands up a local backend and points `TESTING` at it.

> **TL;DR**
> ```bash
> supabase start # boot the local stack (first run pulls Docker images)
> supabase status # copy the API URL + anon key
> # put them in local.properties as supabase.testing.* (see below), then:
> ./gradlew :client:composeApp:run # desktop, talks to localhost:54321
> ```
> In the app: **More → Developer Settings → Environment → TESTING**, then restart.

---

## Why this works without app code changes

The client already has the wiring:

- `Environment` enum (`PROD` / `TESTING` / `FAKE`) + `EnvironmentProvider` in `client/shared`.
- `SupabaseModule` reads `BuildConfig.SUPABASE_TESTING_URL` / `_KEY` when the active env is
`TESTING` ([SupabaseModule.kt](../client/auth/data/impl/src/commonMain/kotlin/com/plusmobileapps/chefmate/auth/data/impl/SupabaseModule.kt)).
- BuildKonfig reads `supabase.testing.url` / `supabase.testing.key` (falling back to the prod
values if unset) — see [client/shared/build.gradle.kts](../client/shared/build.gradle.kts).
- The **Developer Settings** screen switches the active env and lets you log in as pre-baked
test users — see [developer-settings.md](developer-settings.md).

So "develop against local" = point `supabase.testing.*` at the local stack and switch the app to
`TESTING`.

---

## Prerequisites

| Tool | Check | Install |
|---|---|---|
| Supabase CLI | `supabase --version` | `brew install supabase/tap/supabase` |
| Docker (running) | `docker info` | Docker Desktop / OrbStack |

The repo is already initialized for the CLI — `supabase/config.toml` is committed. You do **not**
need to run `supabase init` again.

---

## ⚠️ One-time: dump prod's schema as a baseline

**This step is required before the local DB will build** — `supabase/migrations/` ships empty.

Why a squashed baseline instead of replaying migrations: the prod schema was built largely by
hand on the dashboard, so the base tables (`recipes`, `profiles`, grocery, `meal_plans`) never had
migration files. The original incremental migrations are kept in
[`supabase/archived_migrations/`](../supabase/archived_migrations/) but are **not** applied —
replaying them fails (the oldest already `REFERENCES recipes(id)` before any file creates it, and
three share the version `20260610`). Instead we snapshot prod's *current* schema into one baseline.

`db dump` is a read-only `pg_dump` — it never modifies prod and doesn't care about migration
history:

```bash
# 1. Authenticate the CLI (opens a browser, one-time).
supabase login

# 2. Link this repo to the prod project. The ref is in your prod dashboard URL:
# https://app.supabase.com/project/<project-ref>
supabase link --project-ref <your-prod-ref>

# 3. Snapshot prod's public schema into an early-timestamped baseline migration so it runs first.
supabase db dump --linked --schema public -f supabase/migrations/20260101000000_baseline.sql
```

This single file recreates everything currently in prod (base tables + every change the archived
migrations made). Commit it. Future schema changes go in new migrations stacked on top.

> **Note on storage buckets.** `db dump --schema public` doesn't include storage. The
> `recipe-photos` and `avatars` buckets + their RLS policies are recreated locally by
> [`supabase/seed.sql`](../supabase/seed.sql) (mirroring `docs/supabase-storage-setup.sql` /
> `docs/supabase-avatars-setup.sql`, which you pasted into the prod dashboard). Nothing extra to do.

---

## Start the stack

```bash
supabase start
```

First run pulls several GB of Docker images (slow); subsequent starts are seconds. When it
finishes it prints your local credentials. Re-print them any time with:

```bash
supabase status
```

Example output:

```
API URL: http://127.0.0.1:54321
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
Studio URL: http://127.0.0.1:54323
Inbucket URL: http://127.0.0.1:54324
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

| Service | URL | Use |
|---|---|---|
| API (REST/Auth/Storage) | `http://localhost:54321` | what the app points at |
| Studio (dashboard) | `http://localhost:54323` | browse tables, run SQL, view storage |
| Inbucket (email capture) | `http://localhost:54324` | read confirmation/magic-link emails |
| Postgres | `localhost:54322` | direct `psql` access (user/pass `postgres`) |

> The local `anon key` is generated from the local JWT secret and is **stable across restarts**
> on your machine, but can differ between CLI versions — always copy it from `supabase status`
> rather than hardcoding.

Apply migrations + `seed.sql` to a clean database at any time:

```bash
supabase db reset
```

---

## Point the app at the local stack

Add the local API URL + anon key to **`local.properties`** (gitignored). The host you use
**depends on the target platform** (see the next section).

```properties
# Desktop (JVM) and iOS simulator can use localhost directly:
supabase.testing.url=http://localhost:54321
supabase.testing.key=<anon key from `supabase status`>

# Pre-baked test users for Developer Settings → "Login as test user".
# These must match the users seeded in supabase/seed.sql.
chefmate.user.1=alice@chefmate.test
chefmate.user.password.1=password123
chefmate.user.2=bob@chefmate.test
chefmate.user.password.2=password123
```

`supabase.testing.*` and `chefmate.user.*` are read at build time, so **rebuild/re-run** after
editing `local.properties`. Full reference: [buildconfig-setup.md](buildconfig-setup.md) and
[developer-settings.md](developer-settings.md).

### Per-platform networking

The local stack listens on your Mac's `localhost`. How each client reaches it differs:

| Target | `supabase.testing.url` | Notes |
|---|---|---|
| **Desktop (JVM)** | `http://localhost:54321` | Runs on the host — works directly. |
| **iOS simulator** | `http://localhost:54321` | Shares the host loopback. HTTP to loopback is exempt from App Transport Security, so no Info.plist change. |
| **Android emulator** | `http://10.0.2.2:54321` | `10.0.2.2` is the emulator's alias for host loopback. Cleartext HTTP is allowed in **debug** builds via [`network_security_config.xml`](../client/composeApp/src/debug/res/xml/network_security_config.xml). |
| **Physical device** | `http://<your-mac-LAN-IP>:54321` | e.g. `http://192.168.1.20:54321`. Device + Mac must be on the same network. You may need `supabase start` exposed on `0.0.0.0` (it binds all interfaces by default). Android still needs the debug cleartext config (the LAN IP is covered only if you add it to the config). |

> **Heads-up:** `supabase.testing.url` is a single build-time value, so building for the Android
> emulator (`10.0.2.2`) vs Desktop/iOS (`localhost`) means swapping the line in `local.properties`
> and rebuilding. Easiest day-to-day loop is **Desktop** or the **iOS simulator** with `localhost`.

---

## Switch the running app to TESTING

1. Build/run a **debug** build (`./gradlew :client:composeApp:run`, `installDebug`, or the iOS
debug scheme).
2. **More tab → Developer Settings** (debug-only row at the bottom).
3. **Environment → TESTING.** This signs you out, wipes the local cache, and prompts for a
restart (the Supabase client binds its URL at first injection, so a restart is required).
4. Reopen the app. Sync now hits your local stack.
5. **Login as test user → User 1** to sign in as `alice@chefmate.test` without typing creds.

---

## Edge Functions locally

The repo ships two functions (`delete-account`, `cleanup-avatars`). Serve them locally:

```bash
supabase functions serve # serves all functions with hot reload
```

They're reachable at `http://localhost:54321/functions/v1/<name>` and the app's `Functions`
client picks them up automatically when pointed at the local API URL. `SUPABASE_URL` and
`SUPABASE_SERVICE_ROLE_KEY` are injected automatically for local serves.

---

## Everyday commands

| Goal | Command |
|---|---|
| Start the stack | `supabase start` |
| Stop it (keeps data) | `supabase stop` |
| Stop + wipe all local data | `supabase stop --no-backup` |
| Reset DB to migrations + seed | `supabase db reset` |
| Show URLs + keys | `supabase status` |
| Tail logs | `supabase logs` (or per service in Docker) |
| New migration from a Studio change | `supabase db diff -f <name>` |
| Serve edge functions | `supabase functions serve` |

---

## Troubleshooting

- **`db reset` fails on `relation "recipes" does not exist`** (or `migrations/` is empty) — you
skipped the baseline step. Run the
[`supabase db dump`](#️-one-time-dump-prods-schema-as-a-baseline) step above.
- **Android emulator: `CLEARTEXT communication ... not permitted`** — you're on a release build,
or using a host other than the ones in the debug `network_security_config.xml`. Use a debug
build and `10.0.2.2` (or add your host to the config).
- **App still hits prod after switching to TESTING** — the Supabase client binds at first
injection. Fully restart (force-stop) the app after the env switch.
- **Login as test user fails** — confirm `supabase/seed.sql` ran (`supabase db reset`) and that
`chefmate.user.*` in `local.properties` matches the seeded emails/passwords, then rebuild.
- **Email confirmation blocking signup** — local `config.toml` sets
`[auth.email] enable_confirmations = false`; any emails that are sent are captured by Inbucket
at `http://localhost:54324` (nothing leaves your machine).
- **Anonymous bootstrap fails** — `config.toml` has `enable_anonymous_sign_ins = true`; if you
changed it, restart the stack.

---

## Alternative: a hosted staging project

If you'd rather not run Docker, create a **second Supabase cloud project** as staging:

1. Create the project in the dashboard; copy its URL + anon key.
2. `supabase link --project-ref <staging-ref>` then `supabase db push` to apply migrations
(after the baseline migration exists). Run `docs/supabase-storage-setup.sql` and
`docs/supabase-avatars-setup.sql` in its SQL editor, and enable **Anonymous Sign-ins** under
Authentication → Providers.
3. Put the staging URL/key in `supabase.testing.*` and switch the app to `TESTING`.

Trade-offs: real email/OAuth and no Docker, but it uses cloud quota and needs a network. The
local stack is preferred for day-to-day work.

---

## Appendix: reconciling prod's migration history (for future CLI pushes)

You've been applying SQL to prod **by hand**, so prod's migration-history table
(`supabase_migrations.schema_migrations`) doesn't know about your migrations. If you ever run
`supabase db push` against prod, the CLI will try to **replay the baseline from scratch** — which
will error because every object already exists.

Because we squashed to a single baseline, reconciling is now a one-liner. After
`supabase link --project-ref <prod-ref>`:

```bash
# Mark the baseline as already-applied WITHOUT re-running it (prod already has this schema).
supabase migration repair --status applied 20260101000000

# Verify local and remote agree.
supabase migration list
```

Once reconciled, `supabase db push` will only apply genuinely *new* migrations you add on top of
the baseline, and you can stop pasting SQL into the dashboard. **This is optional and only needed
when you want the CLI to manage prod — it is not required for local development.**

> The archived per-feature migrations in `supabase/archived_migrations/` are history-only and are
> never pushed; the baseline already contains their combined effect.
8 changes: 8 additions & 0 deletions supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Supabase
.branches
.temp

# dotenvx
.env.keys
.env.local
.env.*.local
16 changes: 16 additions & 0 deletions supabase/archived_migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Archived migrations

These are the original incremental migration files that predate adopting the Supabase CLI.

They are **not** applied by the CLI (they live outside `supabase/migrations/`, so `supabase db
reset` / `db push` ignore them). They were superseded by the squashed baseline migration
(`supabase/migrations/<timestamp>_baseline.sql`), which is a full snapshot of the prod schema and
already includes every change these files made.

Why squashed: the prod schema was built largely by hand via the dashboard, so the base tables
(`recipes`, `profiles`, grocery, `meal_plans`) never had migration files — the oldest file here
already `REFERENCES recipes(id)`. Replaying these against a fresh DB therefore fails on ordering,
and three of them share the version `20260610` (a duplicate-version collision in the history
table). The baseline sidesteps both problems.

Kept here purely for historical reference. See [docs/supabase-local-development.md](../../docs/supabase-local-development.md).
Loading
Loading