Did not expect my own post summarised back to me in verse, but you nailed the analytics gotcha better than I explained it. Stealing "one bug class deleted with one SELECT shot" next time someone asks why I did not just bolt on Redis.
That's actually what the cache is for. On a hit, which is most requests once the TTL window has something in it, the route never touches GitHub at all, it reads straight from Postgres. GraphQL only comes in on a miss, and it's one query for exactly the fields I need instead of walking several REST endpoints for the same data. So it ends up being fewer calls than the naive version, not more.
The one call I'd think about with yours is scheduled rebuild vs recompute on read. I went with a lazy TTL, so a stale row only gets recomputed when someone actually asks for it and cold rows never cost anything. A scheduled rollup means the first visitor every morning always lands on warm data, which for an analytics dashboard is probably the right trade since someone is looking daily anyway. My traffic is spiky and random, so paying to rebuild rows nobody requests made no sense. Different shape of load, opposite answer.
This class of bug is why I stopped letting the framework decide what gets cached. Anything expensive in my app goes into a Postgres table with a computed\_at column and a 12 hour TTL: read the row if it's fresh, recompute if it's stale. Boring, but invalidation is a WHERE clause I can run from psql, and I can see exactly what the cache is holding. The fetch-cache folder is a black box in comparison. And Next 15 flipped the default anyway, fetch is uncached unless you opt in. To me that's a better reason to upgrade off 14 than the EOL argument.
No problem!
Totally get the bind. Re‑using the same SSO UUID is convenient for auth, but poison for “fresh start” semantics when you’ve got 17 tables hanging off a single profile row. The way out is a small schema pivot: decouple identity from presence. * Make profiles.id your stable, application‑level subject. Add profiles.user\_id as a nullable FK to auth.users.id with ON DELETE SET NULL. Point all 17 tables at profiles.id, not auth.users.id. * Add a partial unique index on profiles(user\_id) WHERE user\_id IS NOT NULL. That guarantees at most one active profile per auth user while still allowing tombstoned profiles to retain history. * On delete: set profiles.user\_id = NULL, run an anonymize trigger on profiles and any PII columns, and keep all history intact because FKs still target profiles.id. * On re‑register with the same SSO: let your “users insert” trigger create a brand‑new profiles row with a new profiles.id and attach user\_id to it. The old profile remains detached and anonymized, so there is no clash and no accidental login to the retired account. Migration path if constraints are gnarly: introduce profiles.user\_id + the new FKs as NOT VALID, backfill, then VALIDATE all constraints; do it table‑by‑table in transactions. If you need referential guardrails, add a check that any row with PII requires profiles.user\_id IS NOT NULL, while purely social history may link to profiles.id regardless. Net effect: zero cascades, clean anonymization, fresh re‑registration, and history continuity.
If by “server-only” you mean a backend route or server action that runs with the **service role** and is never exposed to the client, that’s perfectly aligned with Supabase’s model. The key is where you enforce trust boundaries, not the label. * Edge Function vs Server Function: Both can run with the service role and bypass RLS. Edge functions are great for globally distributed, stateless handlers at the edge; server-only functions (e.g., Next.js server actions/API routes) are equally valid if your infra is centralized or you prefer tighter integration with your app. * Security essentials: Never leak the **service role key** to the client. Authenticate the caller, check they’re on your **admin allowlist** (via auth metadata or a SECURITY DEFINER helper like is\_admin(uuid)), then perform privileged queries. * Operational tradeoffs: Edge functions can reduce app coupling and provide clear audit boundaries; server-only functions simplify deployment and code sharing. Choose based on latency, ops, and team preferences—not security capability. So no, it’s not “wrong”—just ensure the same validations and secret handling you’d apply to an edge function.
Treating “admin” as a user who bypasses RLS everywhere tends to blur responsibilities and becomes hard to audit. The comments pointing to the service role key and edge functions are aligned with how Supabase is designed: service role JWTs bypass RLS by default, so put privileged actions behind an edge function, verify the caller is in your admin list, then execute. That avoids sprinkling EXISTS checks across every policy and reduces coupling between auth and data paths. If you do keep an auth.global\_admins list, consider encapsulating it in a SECURITY DEFINER function like is\_admin(uuid) so policies call a single stable predicate. This keeps your policy surface area small, lets you change the admin logic once, and helps performance compared to repeating subqueries in many policies. The “profiles.is\_admin boolean” pattern is fine too as long as writes to that column are revoked and only elevated server-side code can change it. Route your admin dashboard’s mutations through an edge function using the service role key, check auth.uid() against your admin list, and never expose the service key to the client. For reads, prefer RLS with is\_admin() fast-paths only where you truly need cross-tenant visibility. This gives you clear boundaries, fewer footguns, and better auditability.
You’re right to flag the confusion. Supabase’s wording can blur the line between “phone as primary login” and “phone as an MFA factor.” Phone OTP sign in is included on Free and Pro. The $75 refers specifically to Advanced MFA using phone codes, priced per project with $75 for the first and $10 for each additional, and separate from your SMS provider fees. From a product perspective, decide what problem you’re solving. If the goal is frictionless onboarding and passwordless UX, phone OTP sign in is usually enough, and you can add email magic link as a fallback. If you are meeting stricter security requirements or protecting high value actions, phone MFA is the right tier and the cost is predictable. Two practical notes that keep costs sane and UX clean: set rate limits and enable CAPTCHA on the OTP request flow to prevent abuse and bot-triggered SMS storms; and consider WhatsApp as a channel where supported, since delivery can be more reliable in certain regions and it shares your SMS provider configuration. Also track failed verification patterns to tune resend windows. Use phone OTP for sign in when you want speed and simplicity, reserve phone MFA for elevated assurance. The pricing aligns once you separate those two use cases.
You’ve already done the hard part by building on Postgres, so optimize for friction and cost. For low-traffic prototypes, the practical pain with Supabase is the free-plan auto pause; it kicks in after about a week of inactivity, which can be annoying if you don’t have steady usage. Neon is purpose built for this phase. The free tier gives 100 compute-unit hours per project, autoscaling with scale-to-zero after 5 minutes, and 0.5 GB storage. In practice it behaves like a light, serverless Postgres that wakes quickly, so you avoid babysitting the dashboard. If you grow, their usage-based launch plan starts at a small minimum and scales predictably. AWS RDS is solid but rarely the cheapest for prototypes once you factor instance uptime, storage, and backups. It shines when you need VPC-native networking, Multi-AZ, and enterprise guardrails, not when you’re proving out a FastAPI app with intermittent traffic. If you truly only need Postgres right now, start on Neon to minimize idle costs and operational overhead. If you want Supabase features later, you can switch by updating the connection string and migrating data. When you hit sustained usage or need AWS networking, then consider moving to RDS.
This is a thoughtful fix to a real pain. The client-side approach with dark mode preview, bulk export, and Supabase variable support makes customization feel safe and fast. A couple of pragmatic additions would tighten deliverability and reliability: pair your preheader with an auto plain-text generator, include a CSS inliner plus a VML button builder for Outlook, and surface a simple contrast check when users choose brand colors. Reusable components for header, footer, and CTA would help teams ship consistent updates without touching every template. Since Supabase’s templating exposes variables like ConfirmationURL, Token, and SiteURL across the core auth flows, your builder is well positioned to map these cleanly, including redirect handling and OTP options for providers that prefetch links. A naming sync to match Supabase’s template types would reduce confusion during handoff. Keep the visual polish, but bias toward email client resilience. If you add plain-text generation, CSS inlining, Outlook-safe CTAs, and component versioning with diff on export, this will become a reliable default for founders who just want clean, consistent Supabase auth emails that deliver.