feat(supabase): add Google/Apple OAuth and fueltide.io-branded email flows

Enables the auth providers and transactional email flows the self-hosted
Supabase was missing compared to the cloud instance:

- GoTrue now accepts Google and Apple OAuth (web flow); Apple client-secret
  JWT is signed fresh on every activation from the SOPS-stored .p8 so
  there's no 6-month rotation ritual.
- SMTP points at mail.cloonar.com:587 with SASL auth via a new `supabase`
  LDAP account; a `noreply@fueltide.io` mailAlias lets that account send
  as the fueltide.io address.
- rspamd on mail.cloonar.com gets a per-domain DKIM key for fueltide.io
  (selector `default`) so outbound mail is signed.
- MAILER_AUTOCONFIRM is off so signup confirmation + password reset
  actually go through email.
- SITE_URL + URI_ALLOW_LIST point at app.fueltide.io / stage so links in
  emails and OAuth redirects land in the right app.

FUELTIDE_AUTH_SETUP.md documents the manual steps (LDAP entries, SOPS
additions, DNS records, Google/Apple console setup) that must be completed
before merging.
This commit is contained in:
Dominik Polakovics Polakovics 2026-04-22 22:08:29 +02:00
parent 1f5e5b9a37
commit 67e81d39f3
5 changed files with 366 additions and 12 deletions

View file

@ -19,11 +19,14 @@ in
sops.secrets.supabase-env = { };
# --- Persistent data directories ---
# Postgres data lives in a named podman volume (supabase-db-data) so podman
# owns the permissions on the container's postgres UID; logical dumps go to
# /var/backups/supabase where borg picks them up from /var.
systemd.tmpfiles.rules = [
"d /var/lib/supabase/db/data 0700 root root -"
"d /var/lib/supabase/storage 0755 root root -"
"d /var/lib/supabase/functions 0755 root root -"
"d /var/lib/supabase/snippets 0755 root root -"
"d /var/backups/supabase 0700 root root -"
];
@ -67,7 +70,12 @@ in
supabase-env-generate = {
description = "Generate Supabase per-container env files from SOPS secrets";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.jq ];
# python+cryptography is used to sign the Apple OAuth client-secret JWT
# (ES256) inside env-generate.sh.
path = [
pkgs.jq
(pkgs.python3.withPackages (ps: [ ps.cryptography ]))
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
@ -95,9 +103,38 @@ in
after = [ "supabase-functions-seed.service" ];
requires = [ "supabase-functions-seed.service" ];
};
# Logical daily dump of the containerised Postgres cluster. Writes to
# /var/backups/supabase which is covered by the borg path /var;
# /var/lib/containers (the named-volume storage) is excluded from borg,
# so the dump is the only copy borg ships off-host.
supabase-db-backup = {
description = "pg_dumpall of the Supabase Postgres cluster";
after = [ "podman-supabase-db.service" ];
requires = [ "podman-supabase-db.service" ];
serviceConfig = {
Type = "oneshot";
};
script = ''
set -euo pipefail
tmp=/var/backups/supabase/supabase-all.sql.tmp
out=/var/backups/supabase/supabase-all.sql
${pkgs.podman}/bin/podman exec -u postgres supabase-db \
pg_dumpall -U postgres --clean --if-exists > "$tmp"
mv "$tmp" "$out"
'';
};
}
]);
systemd.timers.supabase-db-backup = {
description = "Daily Supabase Postgres dump";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 02:30:00";
Persistent = true;
};
};
# --- Containers ---
virtualisation.oci-containers.containers = {
@ -114,7 +151,7 @@ in
};
environmentFiles = [ "/run/supabase/db.env" ];
volumes = [
"/var/lib/supabase/db/data:/var/lib/postgresql/data"
"supabase-db-data:/var/lib/postgresql/data"
"${./sql/_supabase.sql}:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:ro"
"${./sql/realtime.sql}:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro"
"${./sql/logs.sql}:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro"
@ -166,8 +203,8 @@ in
GOTRUE_API_PORT = "9999";
API_EXTERNAL_URL = "https://supabase.cloonar.com";
GOTRUE_DB_DRIVER = "postgres";
GOTRUE_SITE_URL = "https://supabase.cloonar.com";
GOTRUE_URI_ALLOW_LIST = "";
GOTRUE_SITE_URL = "https://app.fueltide.io";
GOTRUE_URI_ALLOW_LIST = "https://app.fueltide.io,https://app.fueltide.io/**,https://app.stage.fueltide.io,https://app.stage.fueltide.io/**,io.fueltide.workout://";
GOTRUE_DISABLE_SIGNUP = "false";
GOTRUE_JWT_ADMIN_ROLES = "service_role";
GOTRUE_JWT_AUD = "authenticated";
@ -175,19 +212,21 @@ in
GOTRUE_JWT_EXP = "3600";
GOTRUE_EXTERNAL_EMAIL_ENABLED = "true";
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED = "false";
GOTRUE_MAILER_AUTOCONFIRM = "true";
GOTRUE_SMTP_ADMIN_EMAIL = "admin@cloonar.com";
GOTRUE_SMTP_HOST = "supabase-mail";
GOTRUE_SMTP_PORT = "2500";
GOTRUE_SMTP_USER = "";
GOTRUE_SMTP_PASS = "";
GOTRUE_SMTP_SENDER_NAME = "Supabase";
GOTRUE_MAILER_AUTOCONFIRM = "false";
GOTRUE_SMTP_ADMIN_EMAIL = "noreply@fueltide.io";
GOTRUE_SMTP_HOST = "mail.cloonar.com";
GOTRUE_SMTP_PORT = "587";
GOTRUE_SMTP_SENDER_NAME = "Fueltide";
GOTRUE_MAILER_URLPATHS_INVITE = "/auth/v1/verify";
GOTRUE_MAILER_URLPATHS_CONFIRMATION = "/auth/v1/verify";
GOTRUE_MAILER_URLPATHS_RECOVERY = "/auth/v1/verify";
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE = "/auth/v1/verify";
GOTRUE_EXTERNAL_PHONE_ENABLED = "false";
GOTRUE_SMS_AUTOCONFIRM = "false";
GOTRUE_EXTERNAL_GOOGLE_ENABLED = "true";
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI = "https://supabase.cloonar.com/auth/v1/callback";
GOTRUE_EXTERNAL_APPLE_ENABLED = "true";
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI = "https://supabase.cloonar.com/auth/v1/callback";
};
environmentFiles = [ "/run/supabase/auth.env" ];
extraOptions = supabaseNet ++ [