Skip to content

Migrate Firestore access to the Admin SDK and ship default-deny rules - #56

Merged
ameyypawar merged 1 commit into
masterfrom
fix/16-admin-sdk-migration
Aug 9, 2026
Merged

Migrate Firestore access to the Admin SDK and ship default-deny rules#56
ameyypawar merged 1 commit into
masterfrom
fix/16-admin-sdk-migration

Conversation

@ameyypawar

Copy link
Copy Markdown
Collaborator

Fixes #16
Fixes #28

Why

The database was world-readable and world-writable. The deployed rules were still the test-mode default:

match /{document=**} {
  allow read, write: if request.time < timestamp.date(2026, 10, 10);
}

Two separate problems with that. First, anyone on the internet could read and write every document using only the NEXT_PUBLIC_FIREBASE_* config, which ships in the client bundle by design. Second, that rule expires on 2026-10-10 — after which every client request is denied, and because all data access went through the client SDK, the app would have stopped working entirely.

The rules could not simply be tightened, though. All five 'use server' service modules imported the Firebase client SDK, so server-side writes arrived with request.auth == null. Any rule strict enough to block an anonymous attacker was also strict enough to block the application itself. That is why this had to be an SDK migration rather than a rules change.

What changed

New src/lib/firebase-admin.ts — a server-only Admin Firestore singleton. Credentials come from FIREBASE_SERVICE_ACCOUNT_JSON (inline, checked first) or FIREBASE_SERVICE_ACCOUNT_PATH (local file), never from a NEXT_PUBLIC_ variable. Re-initialisation is guarded with getApps().length.

It deliberately does not carry 'use server': Next.js only permits async-function exports from such files, and this module exports a Firestore instance.

All six service modules migrated from the client SDK to Admin — questionService, commentService, eventService, userService, voteService, searchService. The Admin API differs throughout (db.collection(...) chaining, .get(), FieldValue.serverTimestamp(), doc.exists as a property rather than a method), so every call site changed.

searchService.ts gained 'use server' (#28). It was the one module querying Firestore straight from the browser, so it had to move server-side before rules could deny client access outright.

firestore.rules, firestore.indexes.json, firebase.json added so the enforcement boundary and the index definitions live in version control and deploy from the repository instead of existing only in the console.

Two missing composite indexes are now defined and deployed: questions (communityId ASC, createdAt DESC) and events (communityId ASC, dateTime DESC). Both were absent, so community-filtered search and events were throwing failed-precondition — and because the services swallow errors and return [], they were failing silently as empty results.

Verification

The load-bearing evidence is a before/after probe using an anonymous client with only the public config and no sign-in:

Collection Before After
questions 8 docs returned permission-denied
events 3 docs returned permission-denied
users 10 docs returned permission-denied
comments readable permission-denied
votes readable permission-denied

Before this change those reads succeeded, which confirmed the exposure was real rather than inferred from the rule text. They now fail. Meanwhile the same reads through the Admin SDK still return 8/3/10, because the Admin SDK bypasses rules by design — so the application keeps working while external access is closed.

Also checked:

  • npm run build succeeds.
  • npx tsc --noEmit reports exactly 7 errors, identical to the pre-change baseline — 2 in community/[communityId]/page.tsx, 4 in SettingsContent.tsx, 1 in searchService.ts. No new type errors. (Those 7 are tracked in Re-enable TypeScript build errors and fix the 7 current failures #47; next.config.ts currently sets ignoreBuildErrors, so a green build alone would not have proven this.)
  • Dev server boots and serves 200 on /, /qna, /events, /search, both before and after the rules deploy.
  • Both indexes polled until they left BUILDING and the real query shapes stopped returning failed-precondition — "deploy succeeded" alone is not sufficient for indexes.
  • No .env file, service-account key, or private key is committed.

Deployment note

Any hosted environment needs FIREBASE_SERVICE_ACCOUNT_JSON set to the full service-account JSON as a single inline string, with no NEXT_PUBLIC_ prefix. The file-path variant is local-only.

Explicitly out of scope

The diff is deliberately narrow — an SDK swap plus the directive, with no behavioural changes — so it can be reviewed as one mechanical transformation. Left untouched on purpose:

Server-side service modules were using the client Firestore SDK, so
privileged writes reached Firestore with request.auth == null. The
deployed rules were the test-mode default (open until 2026-10-10),
which is the only way that could have worked - meaning the database
is world-readable and world-writable today, and would start rejecting
every client request once test mode expires, taking the app down.

- Add firebase-admin and a server-only Admin Firestore singleton
  (src/lib/firebase-admin.ts), reading credentials from
  FIREBASE_SERVICE_ACCOUNT_PATH (local file) or
  FIREBASE_SERVICE_ACCOUNT_JSON (inline, for Vercel), never from a
  NEXT_PUBLIC_ var.
- Switch questionService, commentService, eventService, userService,
  voteService, and searchService from the client SDK to the Admin
  instance. Behavior is unchanged - this is purely the SDK swap.
- Add 'use server' to searchService.ts, which previously had no
  directive and queried Firestore straight from the browser.
- Add firestore.rules (default-deny for all client access - the Admin
  SDK bypasses rules, so server-side reads/writes are unaffected),
  firestore.indexes.json (the missing composite indexes for
  questions on communityId+createdAt and events on
  communityId+dateTime), and firebase.json wiring both.
- Deployed indexes and rules to v-threads. Verified with a live probe:
  anonymous client SDK reads on questions/events/users succeeded
  before the rules deploy and fail with permission-denied after.

Fixes #16
Fixes #28
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vforum Error Error Aug 9, 2026 9:02am

@ameyypawar
ameyypawar merged commit d0b00dc into master Aug 9, 2026
1 of 2 checks passed
@ameyypawar
ameyypawar deleted the fix/16-admin-sdk-migration branch August 9, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add 'use server' to searchService.ts Ship firestore.rules and move privileged writes to the Admin SDK

1 participant