Skip to main content
SupabaseRLSAuthenticationNext.jsSecurity

Supabase RLS and Auth Patterns for a Portfolio Dashboard

elango
elango

Full Stack Engineer

7/13/2026
4 min read
Supabase RLS and Auth Patterns for a Portfolio Dashboard

Public site, private dashboard

elangodev.com is mostly public: blog posts, handbook pages, about, and contact. The dashboard is not. Mixing those trust levels in one Supabase project means every table needs an explicit Row Level Security story. This article covers the patterns I actually run — anon reads for published posts, authenticated writes for drafts, and service-role scripts for editorial rewrites.

Published posts are readable; drafts are not

The posts table stores both published articles and drafts from the Gemini generator. Public routes only query status = 'published'. RLS reinforces that so a miswritten client query cannot leak drafts.

-- Conceptual policy shape
CREATE POLICY "Public can read published posts"
ON posts FOR SELECT
USING (status = 'published');

CREATE POLICY "Owner can manage own posts"
ON posts FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

Admin scripts such as rewrite-original-content.js use the service role key intentionally. Those scripts run locally with dotenv, never from the browser. Shipping the service role to the client would bypass RLS entirely — an easy AdSense-adjacent trust failure if attackers can inject content.

Cookie sessions with @supabase/ssr

Dashboard auth uses cookie-based sessions through @supabase/ssr so Server Components and route handlers share the same user. Login, signup, and reset-password routes stay noindex and are disallowed in robots.ts. That keeps thin auth UI out of the AdSense crawl surface while still protecting mutations.

// Conceptual server client
import { createServerClient } from "@supabase/ssr";

export function createClient(cookieStore) {
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    { cookies: { /* get/set/remove */ } },
  );
}

API routes that must not leak

My AdSense readiness checks flag unauthenticated GETs that expose private data. Contact messages, analytics internals, and draft posts stay behind auth or service role. Public APIs are limited to intentional surfaces (for example, published post lists already available on HTML pages).

When I add a new route under app/api, I ask: would this JSON surprise a visitor if it appeared in Google’s cache? If yes, it needs auth or robots disallow — usually both.

File uploads and signed URLs

The drop/upload flows use short-lived signed URLs to S3 rather than open buckets. Auth checks happen before signing. That keeps user files off the public crawl graph and prevents the portfolio from becoming an open storage host — another low-value / abuse risk for ad networks.

Operational checklist I use before publishing

  • Confirm the post status is published only after human review.
  • Confirm cover images are self-hosted on elangodev.com, not random Unsplash URLs.
  • Run npm run check:adsense so thin drafts and templated seeds surface before reviewers do.
  • Spot-check that /dashboard returns noindex and stays out of the sitemap.

Why this belongs in an AdSense content conversation

Security boundaries sound unrelated to “low value content,” but they decide which URLs exist and who can flood the site with junk. A portfolio that lets anyone publish drafts, or that indexes empty dashboards, looks thin and risky. RLS plus draft-only AI generation is part of how I keep the indexed corpus intentional.

If you run Supabase for a personal brand site, write the policies for the content model you actually have: public articles, private drafts, and privileged scripts — then keep crawlers on the public side only.

Draft-only AI generation as an RLS partner

The blog generate API now inserts status: "draft" instead of publishing immediately. That change matters for RLS and for AdSense. Even if a policy bug momentarily exposed drafts, the indexed corpus would not automatically grow with unreviewed Gemini output. Human publish from the dashboard remains the gate.

Editorial rewrite scripts still use the service role, but they only upsert known high-quality bodies from scripts/content/rewritten/. I keep near-duplicate and off-niche slugs out of that publish set so the public table stays curated.

// Generate path (conceptual)
await adminClient.from("posts").insert({
  ...postData,
  status: "draft", // never auto-publish
});

Pairing draft-only generation with strict SELECT policies is how a solo founder site stays both productive and review-safe. Speed comes from tooling; quality comes from not letting tooling publish unsupervised.

I also keep contact form submissions and private messages behind authenticated dashboard reads. Public contact pages accept input, but listing endpoints must not be anonymously enumerable — another readiness check that protects both privacy and the appearance of a serious site.

Auth and RLS will not invent unique article ideas for you, but they stop low-value surfaces and leaked drafts from undermining the content you do publish. On elangodev.com that boundary is as important as word count.

elango

Written by

elango

Full Stack Engineer & Software Architect specializing in React, Next.js, and high-performance web applications.

Share on:

Discover More

View blog