diff --git a/README.md b/README.md index def5ea029..b51313acd 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/client/composeApp/src/debug/AndroidManifest.xml b/client/composeApp/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..e0106c479 --- /dev/null +++ b/client/composeApp/src/debug/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/client/composeApp/src/debug/res/xml/network_security_config.xml b/client/composeApp/src/debug/res/xml/network_security_config.xml new file mode 100644 index 000000000..2797ae214 --- /dev/null +++ b/client/composeApp/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + 10.0.2.2 + localhost + 127.0.0.1 + + diff --git a/docs/buildconfig-setup.md b/docs/buildconfig-setup.md index 35869bdd8..f2fcff11d 100644 --- a/docs/buildconfig-setup.md +++ b/docs/buildconfig-setup.md @@ -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. diff --git a/docs/developer-settings.md b/docs/developer-settings.md index b95aab347..af0640139 100644 --- a/docs/developer-settings.md +++ b/docs/developer-settings.md @@ -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 diff --git a/docs/supabase-local-development.md b/docs/supabase-local-development.md new file mode 100644 index 000000000..1721ded29 --- /dev/null +++ b/docs/supabase-local-development.md @@ -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/ +supabase link --project-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= + +# 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://: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/` 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 ` | +| 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 ` 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 `: + +```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. diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 000000000..ad9264f0b --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/migrations/20260513_add_categories.sql b/supabase/archived_migrations/20260513_add_categories.sql similarity index 100% rename from supabase/migrations/20260513_add_categories.sql rename to supabase/archived_migrations/20260513_add_categories.sql diff --git a/supabase/migrations/20260522_add_grocery_aisle.sql b/supabase/archived_migrations/20260522_add_grocery_aisle.sql similarity index 100% rename from supabase/migrations/20260522_add_grocery_aisle.sql rename to supabase/archived_migrations/20260522_add_grocery_aisle.sql diff --git a/supabase/migrations/20260602_add_recipe_books.sql b/supabase/archived_migrations/20260602_add_recipe_books.sql similarity index 100% rename from supabase/migrations/20260602_add_recipe_books.sql rename to supabase/archived_migrations/20260602_add_recipe_books.sql diff --git a/supabase/migrations/20260604_feature_flags_admin.sql b/supabase/archived_migrations/20260604_feature_flags_admin.sql similarity index 100% rename from supabase/migrations/20260604_feature_flags_admin.sql rename to supabase/archived_migrations/20260604_feature_flags_admin.sql diff --git a/supabase/migrations/20260605_add_feature_flag_user_ids.sql b/supabase/archived_migrations/20260605_add_feature_flag_user_ids.sql similarity index 100% rename from supabase/migrations/20260605_add_feature_flag_user_ids.sql rename to supabase/archived_migrations/20260605_add_feature_flag_user_ids.sql diff --git a/supabase/migrations/20260608_add_recipe_book_collaboration.sql b/supabase/archived_migrations/20260608_add_recipe_book_collaboration.sql similarity index 100% rename from supabase/migrations/20260608_add_recipe_book_collaboration.sql rename to supabase/archived_migrations/20260608_add_recipe_book_collaboration.sql diff --git a/supabase/migrations/20260610_fix_recipe_rls_recursion.sql b/supabase/archived_migrations/20260610_fix_recipe_rls_recursion.sql similarity index 100% rename from supabase/migrations/20260610_fix_recipe_rls_recursion.sql rename to supabase/archived_migrations/20260610_fix_recipe_rls_recursion.sql diff --git a/supabase/migrations/20260610_invite_email_via_auth_users.sql b/supabase/archived_migrations/20260610_invite_email_via_auth_users.sql similarity index 100% rename from supabase/migrations/20260610_invite_email_via_auth_users.sql rename to supabase/archived_migrations/20260610_invite_email_via_auth_users.sql diff --git a/supabase/migrations/20260610_recipe_book_collaborators.sql b/supabase/archived_migrations/20260610_recipe_book_collaborators.sql similarity index 100% rename from supabase/migrations/20260610_recipe_book_collaborators.sql rename to supabase/archived_migrations/20260610_recipe_book_collaborators.sql diff --git a/supabase/archived_migrations/README.md b/supabase/archived_migrations/README.md new file mode 100644 index 000000000..6a3a07737 --- /dev/null +++ b/supabase/archived_migrations/README.md @@ -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/_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). diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 000000000..73982a3b5 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,416 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "chef-mate" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 +# Controls whether new tables, views, sequences and functions created in the `public` schema by +# `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) +# without explicit GRANTs. Leave unset today to preserve local behaviour. The implicit default +# flips to `false` on 2026-05-30 to match the new cloud default, and the field is removed in +# 2026-10-30 once the always-revoked behaviour is permanent. Set to `false` to opt in early. +# auto_expose_new_tables = false + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +[storage.vector] +enabled = true +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to auth.external_url. +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +# ChefMate bootstraps an anonymous Supabase session on app start +# (SupabaseAuthenticationRepository.kt), so this must stay enabled locally — matching the +# "Anonymous Sign-ins" provider toggle enabled on the prod dashboard. +enable_anonymous_sign_ins = true +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +# Configure passkey sign-ins. +# [auth.passkey] +# enabled = false + +# Configure WebAuthn relying party settings (required when passkey is enabled). +# [auth.webauthn] +# rp_display_name = "Supabase" +# rp_id = "localhost" +# rp_origins = ["http://127.0.0.1:3000"] + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ `{{ .Code }}` }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ `{{ .Code }}` }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth callback URL derived from auth.external_url. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" + +# [experimental.pgdelta] +# When enabled, pg-delta becomes the active engine for supported schema flows. +# enabled = false +# Directory under `supabase/` where declarative files are written. +# declarative_schema_path = "./database" +# JSON string passed through to pg-delta SQL formatting. +# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}" diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 000000000..b8188a2a6 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,162 @@ +-- Local-only seed data, applied by `supabase db reset` / `supabase start`. +-- +-- This file is for the LOCAL Docker stack ONLY. It is never pushed to prod (only +-- `supabase/migrations/` is). It recreates the things that, on prod, were set up by hand: +-- 1. The storage buckets + RLS policies (mirrors docs/supabase-storage-setup.sql and +-- docs/supabase-avatars-setup.sql, which were pasted into the prod dashboard). +-- 2. A couple of confirmed email/password users so the in-app Developer Settings +-- "Login as test user" flow works against the local stack with no network. +-- +-- Keep the test-user credentials below in sync with the `chefmate.user.*` entries in +-- local.properties (see docs/supabase-local-development.md). + +-- --------------------------------------------------------------------------- +-- Storage: recipe-photos bucket + policies +-- --------------------------------------------------------------------------- +insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types) +values ( + 'recipe-photos', + 'recipe-photos', + true, + 5242880, -- 5 MB + array['image/jpeg', 'image/png', 'image/webp', 'image/heic'] +) +on conflict (id) do update set + public = excluded.public, + file_size_limit = excluded.file_size_limit, + allowed_mime_types = excluded.allowed_mime_types; + +drop policy if exists "recipe_photos_public_read" on storage.objects; +create policy "recipe_photos_public_read" +on storage.objects for select +to public +using (bucket_id = 'recipe-photos'); + +drop policy if exists "recipe_photos_owner_insert" on storage.objects; +create policy "recipe_photos_owner_insert" +on storage.objects for insert +to authenticated +with check ( + bucket_id = 'recipe-photos' + and (storage.foldername(name))[1] = auth.uid()::text +); + +drop policy if exists "recipe_photos_owner_update" on storage.objects; +create policy "recipe_photos_owner_update" +on storage.objects for update +to authenticated +using ( + bucket_id = 'recipe-photos' + and (storage.foldername(name))[1] = auth.uid()::text +) +with check ( + bucket_id = 'recipe-photos' + and (storage.foldername(name))[1] = auth.uid()::text +); + +drop policy if exists "recipe_photos_owner_delete" on storage.objects; +create policy "recipe_photos_owner_delete" +on storage.objects for delete +to authenticated +using ( + bucket_id = 'recipe-photos' + and (storage.foldername(name))[1] = auth.uid()::text +); + +-- --------------------------------------------------------------------------- +-- Storage: avatars bucket + policies +-- --------------------------------------------------------------------------- +insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types) +values ( + 'avatars', + 'avatars', + true, + 5242880, -- 5 MB + array['image/jpeg', 'image/png', 'image/webp', 'image/heic'] +) +on conflict (id) do update set + public = excluded.public, + file_size_limit = excluded.file_size_limit, + allowed_mime_types = excluded.allowed_mime_types; + +drop policy if exists "avatars_public_read" on storage.objects; +create policy "avatars_public_read" +on storage.objects for select +to public +using (bucket_id = 'avatars'); + +drop policy if exists "avatars_owner_insert" on storage.objects; +create policy "avatars_owner_insert" +on storage.objects for insert +to authenticated +with check ( + bucket_id = 'avatars' + and (storage.foldername(name))[1] = auth.uid()::text +); + +drop policy if exists "avatars_owner_update" on storage.objects; +create policy "avatars_owner_update" +on storage.objects for update +to authenticated +using ( + bucket_id = 'avatars' + and (storage.foldername(name))[1] = auth.uid()::text +) +with check ( + bucket_id = 'avatars' + and (storage.foldername(name))[1] = auth.uid()::text +); + +drop policy if exists "avatars_owner_delete" on storage.objects; +create policy "avatars_owner_delete" +on storage.objects for delete +to authenticated +using ( + bucket_id = 'avatars' + and (storage.foldername(name))[1] = auth.uid()::text +); + +-- --------------------------------------------------------------------------- +-- Auth: pre-baked, email-confirmed test users +-- --------------------------------------------------------------------------- +-- Inserts directly into GoTrue's tables with a bcrypt-hashed password and a matching +-- `auth.identities` row (required for email/password login on current GoTrue). Emails are +-- pre-confirmed (email_confirmed_at = now()) so no verification step is needed. +do $$ +declare + test_users text[][] := array[ + array['alice@chefmate.test', 'password123'], + array['bob@chefmate.test', 'password123'] + ]; + u text[]; + uid uuid; +begin + foreach u slice 1 in array test_users loop + -- Skip if a user with this email already exists (keeps the seed idempotent). + if exists (select 1 from auth.users where email = u[1]) then + continue; + end if; + + uid := gen_random_uuid(); + + insert into auth.users ( + instance_id, id, aud, role, email, encrypted_password, + email_confirmed_at, created_at, updated_at, + raw_app_meta_data, raw_user_meta_data, is_super_admin + ) values ( + '00000000-0000-0000-0000-000000000000', uid, 'authenticated', 'authenticated', + u[1], extensions.crypt(u[2], extensions.gen_salt('bf')), + now(), now(), now(), + '{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, false + ); + + insert into auth.identities ( + id, user_id, provider_id, identity_data, provider, + last_sign_in_at, created_at, updated_at + ) values ( + gen_random_uuid(), uid, uid::text, + jsonb_build_object('sub', uid::text, 'email', u[1]), 'email', + now(), now(), now() + ); + end loop; +end $$;