Repository navigation
generate cron refactoring #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
51eebff
generate cron refactoring
landsman b3c37d6
review
landsman c436fe9
doc: add missing template
landsman 0ca9df6
paths, colors
landsman fafadd8
use datetime for decision record file
landsman 6239701
generate cron
landsman 28e5df3
fix mcp config
landsman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Title | ||
|
|
||
| <!-- Short, descriptive title of the decision. Avoid "architecture" or "ADR" in the name. --> | ||
|
|
||
| ## Status | ||
|
|
||
| <!-- proposed | accepted | rejected | deprecated | superseded by [NNN](NNN-title.md) --> | ||
|
|
||
| Proposed | ||
|
|
||
| ## Context | ||
|
|
||
| <!-- What problem or situation is motivating this decision? | ||
| What constraints, requirements or observations led here? --> | ||
|
|
||
| ## Decision | ||
|
|
||
| <!-- What is being decided? What change is proposed or already made? --> | ||
|
|
||
| ## Consequences | ||
|
|
||
| <!-- What becomes easier or harder after this decision? | ||
| List both positive and negative consequences. --> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # 003 — Cron Jobs Use Supabase Vault for Secrets (No Hardcoded Keys in Public Repo) | ||
|
|
||
| **Date:** 2026-03-27 | ||
| **Status:** Active | ||
|
|
||
| --- | ||
|
|
||
| ## Problem | ||
|
|
||
| The `cron_jobs.sql` file contained the Supabase `anon` key hardcoded as a bearer token in the HTTP headers of every `net.http_post` call. This file was committed to a public GitHub repository, exposing the key publicly. | ||
|
|
||
| Additionally, cron jobs were not deployed automatically — `cron_jobs.sql` was a manual artifact that had to be applied by hand, with no CI integration. | ||
|
|
||
| --- | ||
|
|
||
| ## Decision | ||
|
|
||
| Store the Supabase `anon` key in **Supabase Vault** (`vault.secrets`) and reference it at cron execution time via `vault.decrypted_secrets`. Cron job definitions live as a standard migration under `supabase/migrations/` and are applied automatically via `supabase db push` in CI. | ||
|
|
||
| The vault secret is upserted by the CI pipeline itself (via `psql` on the self-hosted runner) — before `supabase db push` runs. The anon key is fetched at deploy time from the Supabase Management API using `SUPABASE_ACCESS_TOKEN` (already required for deployment), so no additional GitHub secret is needed. The migration file contains no secret values and is safe to commit. | ||
|
|
||
| --- | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| Supabase Management API | ||
| └── projects api-keys (fetched via SUPABASE_ACCESS_TOKEN at deploy time) | ||
| │ | ||
| ▼ (psql upsert step, before db push) | ||
| Supabase Vault | ||
| └── vault.secrets { name: 'supabase_anon_key', secret: '<anon key>' } | ||
| │ | ||
| ▼ (read at cron execution time by pg_cron) | ||
| pg_cron job SQL | ||
| └── SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'supabase_anon_key' | ||
| ``` | ||
|
|
||
| The migration file stores the cron SQL as a string. pg_cron evaluates it at runtime, so the vault lookup happens each time the cron fires — not at migration time. | ||
|
|
||
| --- | ||
|
|
||
| ## Why anon key and not service_role key | ||
|
|
||
| Cron jobs call Edge Functions via their public HTTPS endpoint. Edge Functions authenticate callers using the anon key (JWT), not the service_role key. The service_role key bypasses RLS at the DB level and is never needed for HTTP-level invocation. Only the anon key is stored in vault for cron use. | ||
|
|
||
| --- | ||
|
|
||
| ## Changing Crons Over Time | ||
|
|
||
| Cron definitions live in `CronDefinition` objects in each Edge Function's sync file. Users are defined in `supabase/functions/_shared/users.ts`. | ||
|
|
||
| After any change, generate and stage a new migration: | ||
|
|
||
| ```bash | ||
| make cron | ||
| ``` | ||
|
|
||
| This runs `scripts/generate-crons.ts`, which writes a timestamped migration to `supabase/migrations/` and prints the path. The Makefile stages it with `git add`. Every generated migration unschedules all existing jobs first, then reschedules them — so no stale jobs can accumulate. | ||
|
|
||
| --- | ||
|
|
||
| ## Local Development | ||
|
|
||
| When working locally with Supabase, seed the vault secret once: | ||
|
|
||
| ```bash | ||
| psql $DATABASE_URL -c "SELECT vault.create_secret('your-local-anon-key', 'supabase_anon_key', 'Supabase anon key for cron HTTP requests');" | ||
| ``` | ||
|
|
||
| The local anon key is found in the output of `supabase status` after `deno task start`. | ||
|
|
||
| --- | ||
|
|
||
| ## Security Properties | ||
|
|
||
| | Property | Before | After | | ||
| |---------------------------------|------------------------|-----------------------------------| | ||
| | Anon key in public repo | Yes (hardcoded in SQL) | No | | ||
| | Anon key in CI logs | N/A | No (GitHub masked secret) | | ||
| | Anon key at rest | Plaintext in git | Encrypted in vault (pgsodium) | | ||
| | Cron deployment | Manual | Automatic via `supabase db push` | | ||
| | Self-hosted runner IP whitelist | N/A | DB accessible only from static IP | | ||
|
|
||
| --- | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| **Template + envsubst in CI** — Replace `${SUPABASE_ANON_KEY}` at CI time and generate the migration file. Rejected: adds fragility (file generation in CI), and the secret is still handled in plaintext in the shell. Vault keeps the secret encrypted at rest in the DB itself. | ||
|
|
||
| **`current_setting()` via `ALTER DATABASE SET`** — Store key as a DB-level setting. Rejected: not encrypted at rest; visible in `pg_settings`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Decision Records | ||
|
|
||
| - [001](001-artist-upsert-anon-role.md) — Artist upsert performance regression caused by missing service role key (anon role fallback) | ||
| - [002](002-reduce-egress.md) — Supabase egress reduction after hitting free-tier bandwidth limit | ||
| - [003](003-cron-vault-secrets.md) — Cron jobs use Supabase Vault for secrets; no hardcoded keys in public repo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| { | ||
| "mcpServers": { | ||
| "supabase": { | ||
| "type": "http", | ||
| "url": "https://mcp.supabase.com/mcp" | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { buildCron, buildUnschedule } from "../supabase/functions/_shared/cron.ts"; | ||
| import { recentTracksCron } from "../supabase/functions/lastfm-user-recent-tracks/sync-tracks.ts"; | ||
| import { libraryArtistsCron } from "../supabase/functions/lastfm-library-artists/sync-artists.ts"; | ||
| import { LASTFM_USERS } from "../supabase/functions/_shared/users.ts"; | ||
|
|
||
| const PROJECT_ID = Deno.env.get("PROJECT_ID"); | ||
| if (!PROJECT_ID) throw new Error("Missing environment variable: PROJECT_ID"); | ||
|
|
||
| const unschedule = LASTFM_USERS.flatMap((user) => [ | ||
| buildUnschedule(recentTracksCron, user), | ||
| buildUnschedule(libraryArtistsCron, user), | ||
| ]); | ||
|
|
||
| const schedule = LASTFM_USERS.flatMap((user) => [ | ||
| buildCron(recentTracksCron, PROJECT_ID, user), | ||
| buildCron(libraryArtistsCron, PROJECT_ID, user), | ||
| ]); | ||
|
|
||
| const output = [ | ||
| "-- unschedule all existing cron jobs before reapplying", | ||
| unschedule.join(""), | ||
| "-- schedule cron jobs", | ||
| schedule.join("\n"), | ||
| ].join("\n"); | ||
|
|
||
| const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14); | ||
| const migrationFile = `supabase/migrations/${timestamp}_cron-jobs.sql`; | ||
|
|
||
| await Deno.writeTextFile(migrationFile, output); | ||
| console.log(migrationFile); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.