Production-leaning full-stack Vision Board app with:
- React + TypeScript + Vite frontend
- Express + TypeScript backend (Cloud Run ready)
- Firebase Auth (Google sign-in)
- Firebase Admin token verification on backend
- Firestore for board/card metadata
- Private AWS S3 bucket with presigned PUT/GET URLs
/Users/bennisevich/eventually
/frontend
package.json
tsconfig.json
tsconfig.app.json
tsconfig.node.json
vite.config.ts
tailwind.config.ts
postcss.config.cjs
index.html
.env.example
/src
main.tsx
App.tsx
index.css
/components
EmptyState.tsx
/lib
api.ts
firebase.ts
types.ts
/backend
package.json
tsconfig.json
Dockerfile
.dockerignore
.env.example
/src
index.ts
app.ts
/config
env.ts
firebase.ts
s3.ts
/lib
async-handler.ts
date.ts
errors.ts
/middleware
auth.ts
error-handler.ts
/routes
boards.routes.ts
storage.routes.ts
/services
boards.service.ts
cards.service.ts
storage.service.ts
/types
express.d.ts
/validators
schemas.ts
- In AWS Console, create an S3 bucket (for example:
vision-board-private-assets). - Keep Block all public access = ON.
- Do not add public bucket policy.
In bucket Permissions -> CORS, set:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "HEAD"],
"AllowedOrigins": [
"http://localhost:5173",
"https://your-frontend-domain.com"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]Create an IAM user or role used by the backend API and attach least-privilege policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisionBoardS3Access",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:AbortMultipartUpload"
],
"Resource": "arn:aws:s3:::vision-board-private-assets/users/*"
}
]
}If you want list/debug access from backend, add:
s3:ListBucketonarn:aws:s3:::vision-board-private-assetswith prefix conditionusers/.
- Go to Firebase Console -> Create project.
- In Authentication -> Sign-in method -> enable Google.
- In Project settings -> add a Web App and copy:
apiKeyauthDomainprojectIdappId
- Firebase Console -> Firestore Database -> Create database.
- Start in production mode.
- Choose region close to Cloud Run region.
- Firebase Console -> Project Settings -> Service Accounts.
- Generate new private key JSON.
- Use these values for backend env:
FIREBASE_PROJECT_IDFIREBASE_CLIENT_EMAILFIREBASE_PRIVATE_KEY(multiline key escaped with\nin.env)
Copy /backend/.env.example to /backend/.env and set:
NODE_ENV=development
PORT=8080
FRONTEND_ORIGINS=http://localhost:5173
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1
S3_BUCKET_NAME=vision-board-private-assets
PRESIGNED_URL_EXPIRES_SECONDS=900
FIREBASE_PROJECT_ID=...
FIREBASE_CLIENT_EMAIL=...
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"Copy /frontend/.env.example to /frontend/.env and set:
VITE_API_BASE_URL=http://localhost:8080
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_APP_ID=...From /Users/bennisevich/eventually:
cd backend && npm install
cd ../frontend && npm installcd /Users/bennisevich/eventually/backend
npm run devcd /Users/bennisevich/eventually/frontend
npm run devOpen http://localhost:5173.
All require:
Authorization: Bearer <Firebase ID Token>
Routes:
POST /boardsGET /boardsGET /boards/:idPOST /boards/:id/cardsGET /boards/:id/cardsPOST /presign-uploadGET /presign-download?key=...
Flow:
- Frontend requests
POST /presign-upload. - Backend verifies Firebase ID token.
- Backend returns presigned PUT URL and key (
users/{uid}/boards/{boardId}/{uuid}.jpg|png|...). - Frontend uploads file directly to S3 (media cards only).
- Frontend saves card metadata via
POST /boards/:id/cardsfor one of:media(requiresimageKey)website(requireswebsiteUrl)text(requirestextContent)
- Board card fetch returns presigned GET URLs for media cards and plain metadata for website/text cards.
cd /Users/bennisevich/eventually/backend
docker build -t vision-board-api:latest .gcloud artifacts repositories create vision-board --repository-format=docker --location=us-central1 --description=\"Vision Board images\"
gcloud auth configure-docker us-central1-docker.pkg.dev
docker tag vision-board-api:latest us-central1-docker.pkg.dev/<GCP_PROJECT_ID>/vision-board/vision-board-api:latest
docker push us-central1-docker.pkg.dev/<GCP_PROJECT_ID>/vision-board/vision-board-api:latestgcloud run deploy vision-board-api \
--image us-central1-docker.pkg.dev/<GCP_PROJECT_ID>/vision-board/vision-board-api:latest \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--port 8080 \
--set-env-vars NODE_ENV=production,PORT=8080,FRONTEND_ORIGINS=https://your-frontend-domain.com,AWS_ACCESS_KEY_ID=...,AWS_SECRET_ACCESS_KEY=...,AWS_REGION=us-east-1,S3_BUCKET_NAME=vision-board-private-assets,PRESIGNED_URL_EXPIRES_SECONDS=900,FIREBASE_PROJECT_ID=...,FIREBASE_CLIENT_EMAIL=...,FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"Notes:
--allow-unauthenticatedkeeps endpoint publicly reachable for browser clients, but all app routes still enforce Firebase ID token auth.- Prefer Secret Manager for sensitive vars (
AWS_SECRET_ACCESS_KEY,FIREBASE_PRIVATE_KEY) in production.
In /frontend/.env set:
VITE_API_BASE_URL=https://vision-board-api-xxxxx-uc.a.run.appThen build/deploy frontend wherever you host static files.
- AWS credentials are backend-only; never exposed in frontend.
- S3 bucket stays private.
- All non-health routes verify Firebase ID token.
- Board/card ownership checks prevent cross-user access.
- Input is validated with Zod.
- Errors are normalized and do not leak internals in production.
- Cloud Run: pay per request/CPU/RAM usage; scales to zero for low idle cost.
- Firestore: pay for document reads/writes/storage; keep payload small and avoid unnecessary polling.
- S3: pay for storage, PUT/GET requests, and egress; presigned URLs keep architecture simple and cheap.
- Firebase Auth: Google sign-in in Firebase Auth is typically low-cost/free in many usage tiers.
- Avoiding ECS/EC2/ALB reduces fixed monthly baseline and ops overhead.