This portfolio is moving from static content to a Firebase-backed, dynamic site with a private FireCMS admin. This guide covers what's built now (contact form) and what's next (dynamic content + admin).
The "Get in Touch" section now has a form that saves submissions to Firestore
(messages collection) and emails you on each new message.
- Go to https://console.firebase.google.com → Add project.
- Once created, Build → Firestore Database → Create database → Production mode → pick a region.
- Build → Authentication → Get started → enable Google (needed later for the admin).
- Build → Storage → Get started (needed later for image uploads from the CMS).
Project settings (gear) → General → Your apps → click the web icon </> to register a web app → copy the firebaseConfig values into .env.local:
cp .env.local.example .env.local
# then paste your values into .env.localThe
NEXT_PUBLIC_FIREBASE_*values are public by design — security is enforced by the Firestore rules below, not by hiding them.
Firestore Database → Rules → paste this, replacing YOUR_ADMIN_EMAIL:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Anyone may submit a contact message (shape-validated). Only you can read them.
match /messages/{id} {
allow create: if
request.resource.data.keys().hasOnly(['name','email','message','createdAt','read','source'])
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 2 && request.resource.data.name.size() <= 100
&& request.resource.data.email is string && request.resource.data.email.size() <= 200
&& request.resource.data.message is string
&& request.resource.data.message.size() >= 10 && request.resource.data.message.size() <= 4000
&& request.resource.data.createdAt == request.time;
allow read, update, delete: if request.auth != null
&& request.auth.token.email == 'YOUR_ADMIN_EMAIL';
}
// Public content (Phase B). Anyone can read; only you can edit (via FireCMS).
match /{col}/{doc} {
allow read: if col in ['profile','socials','skills','work','education','projects','openSource'];
allow write: if request.auth != null
&& request.auth.token.email == 'YOUR_ADMIN_EMAIL';
}
}
}
- Sign up at https://resend.com → API Keys → create one (starts with
re_). - Put it in
.env.localasRESEND_API_KEY, and setCONTACT_TO_EMAILto your inbox. - (Optional) Verify a domain in Resend and set
CONTACT_FROM_EMAIL; otherwise the default test senderonboarding@resend.devis used (it can email your own account).
pnpm devOpen the site, scroll to Get in Touch, submit the form — the message appears in
Firestore (messages) and an email lands in your inbox.
Until
.env.localis filled, the form renders but submitting shows a friendly "reach me by email" message — the site never breaks.
Add the same env vars in Vercel → Project → Settings → Environment Variables, then redeploy.
Goal: edit every section (About, Work, Education, Skills, Projects, Open Source, contact details) from a private admin UI; changes go live immediately.
Planned Firestore data model (one editable place per section):
| Collection / doc | Holds |
|---|---|
profile/main (doc) |
name, headline/description, About summary, avatar, résumé, email, phone, location |
socials |
GitHub / LinkedIn / Email links (label, url, icon, order) |
skills |
the 6 categories, each with its chip items + highlight flags |
work |
roles (company, title, dates, description, logo, badge media) |
education |
schools (name, degree, dates, logo) |
projects |
the project cards (title, type, tags, links, image, order) |
openSource |
contributions (repo, PR count, description, PR list, order) |
messages |
contact-form submissions (Phase A) |
Then:
- A one-time seed script migrates today's content into Firestore.
- The site switches to read from Firestore (server-rendered, always fresh).
- A separate FireCMS app (its own URL, Google-login locked to you) edits these collections — including image uploads to Firebase Storage.
This phase is built once your Firebase project above is live, so it can be verified against your real database (rules, queries, admin auth) rather than guessed.