I spent about a day working on impelemnting comments on the site. Why? One simple reason - traffic! If you’ve been following my journey with Astro, the whole reason I chose it was because it’s optimized for SEO traffic. What it isn’t optmized for is integrating a commens section but honestly, it wasn’t that hard. Here’s what I did…

I built a serverless comments system for this site using Astro + Supabase and shipped a small moderation workflow with email notifications. This post walks through the approach, implementation highlights, and every real-world hiccup I encountered — plus the concrete fixes I used so you can reproduce the setup without repeating these mistakes.

Why this approach


  • Astro provides a fast static content site with lightweight server routes (I use src/pages/api/*).
  • Supabase gives us Postgres, RLS and a familiar SQL surface without managing a DB server.
  • Server-side email (I used Resend + Nodemailer) sends notifications on new comments.
  • Admin moderation uses a tiny HMAC-signed cookie token rather than a full user system.

Architecture overview


  • Public UI: CommentForm posts to POST /api/comments.
  • Server endpoints: GET /api/comments (public — only approved comments), POST /api/comments (validate + insert), and admin endpoints under /api/admin/* for login/logout/moderation.
  • Storage: Supabase comments table (with an approved boolean controlling public visibility).
  • Email: server helper at src/lib/email.ts invoked on new comment creation.

What I implemented (short list)


  • Server-side insert path guarded by a server-only Supabase client (supabaseAdmin).
  • Admin tokens signed with HMAC and optional scrypt-based KDF (via ADMIN_PEPPER).
  • Short-lived admin_token cookie (30 minutes) with HttpOnly, SameSite=Strict, and Secure flags.
  • Honeypot field in the comment form plus server-side check.
  • Lightweight in-memory rate limiter to protect endpoints (admin login and comment POST).
  • Admin UI styled with the site BaseLayout for consistency.
  • Migration plan to remove service_role from app code and move writes to a least-privilege RPC or Edge Function (docs/RPC_EDGE_FUNCTION_PLAN.md).

Detailed walkthrough & hiccups


Below are the real surprises I hit while building and how I fixed them.

  1. Body parsing gotcha: PowerShell and curl
  • Problem: When testing endpoints from PowerShell or various clients I sometimes received malformed JSON or unexpected form-encoded payloads which triggered 400/500 responses.
  • Fix: Make the server tolerant: accept application/json and application/x-www-form-urlencoded, normalise multiple field names, and validate inputs early. When using PowerShell, prefer Invoke-RestMethod with explicit ContentType and JSON conversion:
Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/comments -Body (ConvertTo-Json @{ author = 'You'; content = 'Hi' }) -ContentType 'application/json'

Or with curl:

curl -X POST -H "Content-Type: application/json" -d '{"author":"You","content":"Hi"}' http://localhost:3000/api/comments
  1. Route and 404 surprises
  • Problem: Astro’s endpoints can return 404s when route naming or file placement is off, which sometimes hid actual parsing errors.
  • Fix: Consolidate into a single comments.ts handler that switches on request method. Add dev logging (method + url) to make errors visible while iterating.
  1. Cookie Secure flag vs local dev
  • Problem: Setting the Secure cookie flag blocks cookies over plain HTTP (local dev), leading to confusion when cookies appeared not to be set.
  • Fix: Enforce Secure in production. For local development, either run with local HTTPS (recommended via mkcert) or temporarily allow non-secure cookies (do not leave this in production).
  1. Admin token signing and optional pepper (KDF)
  • What I did: src/lib/admin-token.ts signs tokens with HMAC-SHA256 using ADMIN_SECRET. If ADMIN_PEPPER is set, a derived key is produced with scryptSync(ADMIN_SECRET, ADMIN_PEPPER, 32).
  • Why: Using a pepper + KDF makes a signing key harder to reconstruct from a leaked secret.
  • Note: Store ADMIN_PEPPER and ADMIN_SECRET in your host secret manager — not in source.
  1. Reducing attack window (short-lived admin cookie)
  • Change: Token TTL set to 30 minutes. The login endpoint returns ttlSeconds so clients can show the expiry.
  • Why: A short TTL reduces the impact of a stolen cookie.
  1. Honeypot and spam heuristics
  • Client: src/components/CommentForm.astro contains a visually-hidden website field that real users will not fill.
  • Server: src/pages/api/comments.ts rejects submissions where the honeypot has a value.
  • Why: Low-effort spam defense that keeps UX friction to zero. Combine with length checks and heuristics for higher coverage.
  1. Rate limiting
  • Implemented src/lib/rate-limiter.ts (in-memory) and used it to throttle comment POSTs and admin login attempts (e.g., 10 comments per 10 minutes; 5 login attempts per 15 minutes).
  • Caveat: In-memory rate limiting is not suitable for multi-instance production — use Redis or host-managed rate-limits for production deployments.
  1. Why I still used a service_role key, and how to remove it
  • Short-term: supabaseAdmin (service_role) used in server code to insert comments and perform admin actions.
  • Long-term: see docs/RPC_EDGE_FUNCTION_PLAN.md — two recommended approaches:
    • Postgres SECURITY DEFINER RPC that sanitises and inserts comments and is callable by anon via a constrained RPC.
    • Supabase Edge Function that validates, inserts and handles side-effects, keeping the service key in the function environment only.
  1. Email notifications
  • I used Nodemailer with Resend SMTP to send a notification email on new comments. The helper is src/lib/email.ts and reads RESEND_API_KEY from environment.
  • Tip: Test using the provider’s UI/logs. If mails don’t arrive, check the SMTP logs and server error output.
  1. Secrets & repo hygiene
  • I found .env.local contained keys — a critical risk.
  • Action: add .env.local to .gitignore, rotate any leaked keys (Supabase service_role, Resend API, ADMIN_SECRET), purge sensitive values from history with git-filter-repo or BFG, and update the deployment secrets.
  • Generate secure secrets with these commands:

PowerShell (Windows):

$bytes = New-Object 'System.Byte[]' 32; (New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($bytes); [Convert]::ToBase64String($bytes)

OpenSSL (macOS / Linux):

openssl rand -base64 32

Testing & verification


  • End-to-end checklist: POST a comment (public) → row in comments table → admin receives notification → admin logs in via /admin → approve the comment → public GET returns approved comment.
  • Dev notes: Restart dev server (ntl dev or npm run dev) after applying code changes. If cookie behavior is unexpected, verify HTTPS and cookie flags.

Files to review

- `src/pages/api/comments.ts` — GET/POST + spam checks
- `src/pages/api/admin/login.ts` & `logout.ts` — admin token and cookie handling
- `src/pages/admin/index.astro` — moderation UI (uses `BaseLayout`)
- `src/components/CommentForm.astro` — client form with honeypot
- `src/lib/admin-token.ts` — HMAC + optional scrypt KDF logic
- `src/lib/rate-limiter.ts` — in-memory rate limiter
- `src/lib/supabase-server.ts` — server-only Supabase client (service_role)
- `docs/RPC_EDGE_FUNCTION_PLAN.md` — migration plan for RPC vs Edge Function
  • Rotate the service_role and API keys immediately if they were ever committed.
  • Remove the service_role from app code by implementing an RPC or Edge function.
  • Replace in-memory rate-limiter with Redis before scaling.
  • Add automated E2E tests for the moderation flow and run them in CI.