Astro + TypeScript app for private complaint and suggestion intake, with Firebase Auth, Firestore RBAC, App Check, Cloudinary image uploads, and optional Gemini 2.5 Flash summaries for moderators.
- Astro with the Cloudflare adapter
- TypeScript
- Tailwind CSS
- Firebase Auth and Firestore on Spark
- Firestore Security Rules and indexes
- Firebase App Check
- Cloudinary restricted upload preset
- Zod validation
- Optional Gemini 2.5 Flash summarization
No Firebase Cloud Functions, Firebase Admin SDK, or custom backend are used. Cloudflare Pages Functions are used only for secure server-side routes.
src/
components/ Reusable Astro components
layouts/ Base shell
lib/ Firebase, schemas, Cloudinary, server helpers
pages/ Astro pages and Cloudflare server routes
scripts/ Browser TypeScript flows
styles/ Tailwind globals
firestore.rules Firestore validation and RBAC
firestore.indexes.json Composite indexes
firebase.json Firebase deploy config
.env.example Required environment variables
SECURITY_CHECKLIST.md Deployment and security checklist
reports/{reportId}
type:complaintorsuggestiontitle,description,category,groupisAnonymouscontactName,contactEmail,submitterUidimage: null or Cloudinary metadataimageReviewStatus:none,hidden_until_review,approved,rejectedstatus:new,in_review,resolved,rejectedmoderatorNotescreatedAt,updatedAt
Anonymous reports write submitterUid: null and blank contact fields, so moderators do not receive the submitter identity.
roles/{uid}
role:moderatororsuperuserdisplayNameemailactiveupdatedAt
The first superuser must be bootstrapped manually in Firebase Console.
rateLimits/{uid_yyyy-mm-dd_scope}
uidscope:reportSubmitorcloudinaryUploaddaycountupdatedAt
This avoids storing rate-limit state inside public report documents.
-
Install dependencies:
npm install
-
Copy
.env.exampleto.envand fill in public Firebase and Cloudinary values. -
In Firebase Console:
-
Enable Email/Password Auth for moderators.
-
Enable Anonymous Auth for public submissions.
-
Enable Firestore.
-
Enable App Check enforcement for Firestore after testing.
-
Manually create the first
roles/{uid}document:{ "role": "superuser", "displayName": "Initial Admin", "email": "admin@example.edu", "active": true, "updatedAt": "server timestamp" }
-
-
Deploy Firestore rules and indexes:
firebase deploy --only firestore
-
In Cloudinary:
- Create a restricted preset named like
cetis26_reports_unsigned. - Allow only image uploads.
- Restrict formats to jpg, jpeg, png, webp.
- Set max file size near 2 MB.
- Use a dedicated folder such as
cetis26/reportes. - Treat uploaded images as hidden until a moderator approves visibility in Firestore.
- Create a restricted preset named like
-
In Cloudflare Pages, set the same public env vars plus server-only secrets:
CLOUDINARY_API_KEYCLOUDINARY_API_SECRETCLOUDINARY_UPLOAD_FOLDERGEMINI_API_KEYoptional
-
Build locally:
npm run build
Cloudflare adapter note: Astro 6 may create a SESSION KV binding in generated Cloudflare output. Create a Cloudflare KV namespace and bind it as SESSION if Cloudflare asks for it during deployment.
POST /api/cloudinary-signature: verifies Firebase ID token, permits complaints only, rate-limits by UID/day, and returns Cloudinary upload params. If Cloudinary API credentials are configured, it also signs the upload params server-side.POST /api/gemini-summary: verifies Firebase ID token and moderator/superuser role before calling Gemini.POST /api/moderate-text: verifies moderator/superuser role and returns advisory risk flags.POST /api/check-role: verifies Firebase ID token and returns the caller role.
Gemini summaries are advisory only. Moderators make all final decisions. The detail page also supports a moderator-pasted Gemini key stored only in localStorage.
- Dashboard tables use
limit(20). - Pagination uses
startAfter(lastDocument). - Filters are covered by composite indexes in
firestore.indexes.json. - Public users never read
reports. - Broad realtime listeners are avoided; dashboard reads are paginated one-shot queries.
- Role management lists only 20 recent role documents and is superuser-only.
npm run devOpen the local URL printed by Astro.
Deploy to Cloudflare Pages with:
- Framework preset: Astro
- Build command:
npm run build - Output directory:
dist
The Astro Cloudflare adapter is already configured.