Fusion Edge
Corporate site and a custom content panel for a Guwahati infrastructure & real-estate firm.
- FUNCTION
- Marketing site + staff-run CMS
- CLIENT
- Fusion Edge Solutions · NE India
- STATUS
- In active development
- ROLE
- Backend · CMS · DB · security
00Scope
Mine: the entire backend and data layer — database schema, the registry-driven admin/CMS, Supabase auth + RLS, security hardening, rate limiting, private résumé storage, and the Google Sheets + email integrations.
Not mine: most of the public-facing frontend. I contributed only a small part of it. This sheet covers the work I actually did.
01The problem
A diversified Northeast-India firm — hotels, real estate, retail, transit developments — needed a public presence that non-technical staff could keep current without calling a developer for every copy change.
Project listings, news, events, job postings: all of it had to be editable in-house. Generic CMS platforms were too heavy, too expensive at this scale, or locked behind a managed subscription the client didn't want.
02Approach
A thin custom CMS built directly on Supabase — Postgres, Auth, Storage — instead of a third-party headless CMS. A single TypeScript registry file declares every content type once; all list, form, and detail admin views are generic, so there's no duplicated page per entity.
The whole admin surface runs under the staff member's own auth session, with RLS enforcing access — no service-role key ever reaches the browser. Rate limiting, instead of living in memory (which breaks across serverless instances), is enforced by a SECURITY DEFINER Postgres function that is the sole writer to a zero-policy RLS table.
03How it works
- Visitors hit Next.js App Router pages; featured projects and copy are fetched server-side from Supabase, falling back to hard-coded defaults when no CMS row exists yet.
- Contact and careers forms submit via Server Actions; a per-IP sliding-window check runs in Postgres before any row is inserted.
- Résumés upload to a private Storage bucket; row and file are written atomically — a failed insert removes the orphaned file immediately.
- Staff log in at
/admin(Supabase email auth); the session is validated server-side on every panel request. - Content is edited through registry-driven forms — Tiptap rich text, image/gallery pickers, spec tables; publishing flips a
statuscolumn that RLS turns into public visibility. - New submissions land in a real-time inbox with unread badges; a Google Sheets mirror and a Resend email hook fire best-effort in parallel, so a side-channel failure never blocks the visitor.
04Capabilities
- CMS for projects, news, events, jobs, team, pages
- Staff inbox: enquiries + applications, unread badges
- Private résumé storage, signed-URL staff download
- Per-IP rate limiting at the Postgres layer
- Honeypot + MIME/size limits, app & bucket layer
- Google Sheets mirror + Resend email, best-effort
05Stack
- LANGUAGE
- TypeScript 5
- FRONTEND
- Next.js 16 (App Router) · React 19 · Tailwind CSS 4 · Framer Motion 12
- BACKEND / DATA
- Supabase (Postgres + Auth + Storage) · Tiptap 3 · Zod 4 · sanitize-html
- SERVICES
- Google Sheets API (mirror) · Resend (email)
- INFRA / DEPLOY
- Vercel (x-forwarded-for IP extraction pattern)
06Engineering notes
-
Rate limiting across serverless. In-memory counters reset on every cold start, so a
SECURITY DEFINERfunction (check_rate_limit) writes to a private table with RLS enabled and zero policies — write-only through the function, unreachable to anon/authenticated roles directly. -
Anon insert without RETURNING. RLS lets anon INSERT to submission tables but grants no SELECT; adding
.select()would trigger a RETURNING clause that fails the policy check. The server action explicitly avoids it — a subtle constraint documented in the migration. -
Two-layer storage hardening. App-level MIME/size validation covers the happy path; Supabase bucket-level
file_size_limitandallowed_mime_typescover direct Storage REST callers who bypass the app entirely with the publishable key. -
Media enumeration closed. The public
mediabucket serves objects by URL with no SELECT policy; the original broad SELECT policy allowed listing every object in the bucket. Dropping it closed that vector with no functional impact.
07What's next
- Leaked-password protection and MFA for admin accounts — flagged in the security audit.
- Activate the Google Sheets mirror once the GCP service account is provisioned.