Loading...
;
+
+ return (
+
+ {messages.map((msg) => (
+
+ {msg.user?.name}: {msg.body}
+
+ ))}
+
+ );
+}
+```
+
+## Deployment
+
+```bash
+# Deploy to production
+npx convex deploy
+
+# Deploy with environment variables
+npx convex deploy --env-file .env.production
+
+# Set environment variables
+npx convex env set ANTHROPIC_API_KEY sk-ant-...
+npx convex env list
+
+# View logs
+npx convex logs
+npx convex logs --follow
+
+# Run a function manually
+npx convex run messages:listByChannel '{"channelId": "abc123"}'
+```
+
+## File Storage
+
+```typescript
+// convex/files.ts
+import { mutation, query } from "./_generated/server";
+import { v } from "convex/values";
+
+export const generateUploadUrl = mutation(async (ctx) => {
+ return await ctx.storage.generateUploadUrl();
+});
+
+export const getFileUrl = query({
+ args: { storageId: v.id("_storage") },
+ handler: async (ctx, args) => {
+ return await ctx.storage.getUrl(args.storageId);
+ },
+});
+```
+
+## Best Practices
+
+- Define schema and validation before writing functions
+- Keep mutations idempotent where possible
+- Use auth identity checks in every privileged query/mutation
+- Add indexes early for high-read collections
+- Use `internal` functions for server-only logic (crons, webhooks)
+- Store secrets in Convex environment variables, never in code
+- Use optimistic updates in the React client for instant UI feedback
+
+## Troubleshooting
+
+| Issue | Solution |
+|-------|---------|
+| Function timeout | Actions have 10min limit; break into smaller steps |
+| Query too slow | Add database index matching your query pattern |
+| Type errors | Run `npx convex dev` to regenerate types |
+| Auth not working | Check `auth.config.ts` and provider domain |
+| Deploy fails | Check `npx convex logs`, verify env vars are set |
## Related Skills
-- [firebase-app-platform](../firebase-app-platform/) - Alternative managed backend
-- [agent-observability](../../../devops/ai/agent-observability/) - Instrument AI-driven backend flows
+- [firebase-app-platform](../firebase-app-platform/) β Alternative managed backend
+- [vercel-deployments](../vercel-deployments/) β Frontend hosting
+- [agent-observability](../../../devops/ai/agent-observability/) β Instrument AI-driven backend flows
diff --git a/infrastructure/platforms/firebase-app-platform/SKILL.md b/infrastructure/platforms/firebase-app-platform/SKILL.md
index ab06148..fd22669 100644
--- a/infrastructure/platforms/firebase-app-platform/SKILL.md
+++ b/infrastructure/platforms/firebase-app-platform/SKILL.md
@@ -1,6 +1,6 @@
---
name: firebase-app-platform
-description: Build and operate apps on Firebase using Auth, Firestore, Cloud Functions, and Hosting.
+description: Build and operate apps on Firebase using Auth, Firestore, Cloud Functions, and Hosting. Use when building mobile/web backends with managed services, real-time data sync, or serverless APIs.
license: MIT
metadata:
author: devops-skills
@@ -11,23 +11,354 @@ metadata:
Ship mobile and web backends with Firebase managed services.
-## Core Setup
+## When to Use This Skill
+
+Use this skill when:
+- Building mobile or web apps with real-time data sync
+- Need authentication with minimal backend code
+- Prototyping quickly with managed infrastructure
+- Building serverless APIs with Cloud Functions
+- Hosting static sites or SPAs with CDN
+
+## Prerequisites
+
+- Node.js 18+
+- Firebase CLI (`npm install -g firebase-tools`)
+- Google Cloud account (Firebase is part of GCP)
+- A Firebase project (create at console.firebase.google.com)
+
+## Quick Start
```bash
+# Install and authenticate
npm install -g firebase-tools
firebase login
+
+# Initialize in your project directory
firebase init
+# Select: Firestore, Functions, Hosting, Emulators
+
+# Start local emulators
+firebase emulators:start
+
+# Deploy everything
firebase deploy
+
+# Deploy specific services
+firebase deploy --only functions
+firebase deploy --only hosting
+firebase deploy --only firestore:rules
```
-## Security and Scale
+## Firestore Database
-- Write strict Firestore security rules first.
-- Separate environments by Firebase project.
-- Enable budget alerts and quota monitoring.
-- Move privileged logic into Cloud Functions.
+### Security Rules
+
+```javascript
+// firestore.rules
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ // Users can only read/write their own data
+ match /users/{userId} {
+ allow read, write: if request.auth != null && request.auth.uid == userId;
+ }
+
+ // Messages: authenticated users can read, only owner can write
+ match /channels/{channelId}/messages/{messageId} {
+ allow read: if request.auth != null;
+ allow create: if request.auth != null
+ && request.resource.data.userId == request.auth.uid
+ && request.resource.data.body is string
+ && request.resource.data.body.size() <= 5000;
+ allow update, delete: if request.auth != null
+ && resource.data.userId == request.auth.uid;
+ }
+
+ // Admin-only collection
+ match /admin/{document=**} {
+ allow read, write: if request.auth != null
+ && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
+ }
+
+ // Default: deny everything
+ match /{document=**} {
+ allow read, write: if false;
+ }
+ }
+}
+```
+
+### Data Operations
+
+```typescript
+// lib/firestore.ts
+import { getFirestore, collection, doc, setDoc, getDoc,
+ query, where, orderBy, limit, onSnapshot,
+ serverTimestamp, increment } from "firebase/firestore";
+
+const db = getFirestore();
+
+// Create document with auto-ID
+async function createMessage(channelId: string, body: string, userId: string) {
+ const ref = doc(collection(db, "channels", channelId, "messages"));
+ await setDoc(ref, {
+ body,
+ userId,
+ createdAt: serverTimestamp(),
+ });
+ return ref.id;
+}
+
+// Real-time listener
+function subscribeToMessages(channelId: string, callback: (msgs: any[]) => void) {
+ const q = query(
+ collection(db, "channels", channelId, "messages"),
+ orderBy("createdAt", "desc"),
+ limit(50)
+ );
+ return onSnapshot(q, (snapshot) => {
+ const messages = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
+ callback(messages);
+ });
+}
+
+// Atomic counter
+async function incrementViews(postId: string) {
+ await setDoc(doc(db, "posts", postId), {
+ views: increment(1),
+ }, { merge: true });
+}
+```
+
+### Indexes
+
+```json
+// firestore.indexes.json
+{
+ "indexes": [
+ {
+ "collectionGroup": "messages",
+ "queryScope": "COLLECTION",
+ "fields": [
+ { "fieldPath": "channelId", "order": "ASCENDING" },
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
+ ]
+ }
+ ]
+}
+```
+
+## Authentication
+
+```typescript
+// lib/auth.ts
+import { getAuth, signInWithPopup, GoogleAuthProvider,
+ createUserWithEmailAndPassword, signInWithEmailAndPassword,
+ signOut, onAuthStateChanged } from "firebase/auth";
+
+const auth = getAuth();
+
+// Google sign-in
+async function signInWithGoogle() {
+ const provider = new GoogleAuthProvider();
+ const result = await signInWithPopup(auth, provider);
+ return result.user;
+}
+
+// Email/password registration
+async function register(email: string, password: string) {
+ const result = await createUserWithEmailAndPassword(auth, email, password);
+ return result.user;
+}
+
+// Auth state listener
+onAuthStateChanged(auth, (user) => {
+ if (user) {
+ console.log("Signed in:", user.uid, user.email);
+ } else {
+ console.log("Signed out");
+ }
+});
+```
+
+## Cloud Functions
+
+```typescript
+// functions/src/index.ts
+import { onRequest } from "firebase-functions/v2/https";
+import { onDocumentCreated } from "firebase-functions/v2/firestore";
+import { getFirestore } from "firebase-admin/firestore";
+import { initializeApp } from "firebase-admin/app";
+
+initializeApp();
+const db = getFirestore();
+
+// HTTP function (API endpoint)
+export const api = onRequest({ cors: true, region: "us-central1" }, async (req, res) => {
+ if (req.method !== "GET") {
+ res.status(405).send("Method not allowed");
+ return;
+ }
+ const snapshot = await db.collection("posts").orderBy("createdAt", "desc").limit(10).get();
+ const posts = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
+ res.json({ posts });
+});
+
+// Firestore trigger β runs when a new message is created
+export const onMessageCreated = onDocumentCreated(
+ "channels/{channelId}/messages/{messageId}",
+ async (event) => {
+ const data = event.data?.data();
+ if (!data) return;
+
+ // Update channel's last message timestamp
+ await db.doc(`channels/${event.params.channelId}`).update({
+ lastMessageAt: data.createdAt,
+ messageCount: FieldValue.increment(1),
+ });
+
+ // Send notification (example)
+ console.log(`New message in ${event.params.channelId}: ${data.body.substring(0, 50)}`);
+ }
+);
+```
+
+## Hosting
+
+```json
+// firebase.json
+{
+ "hosting": {
+ "public": "dist",
+ "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
+ "rewrites": [
+ { "source": "/api/**", "function": "api" },
+ { "source": "**", "destination": "/index.html" }
+ ],
+ "headers": [
+ {
+ "source": "**/*.@(js|css|svg|png|jpg|webp|woff2)",
+ "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
+ },
+ {
+ "source": "**",
+ "headers": [
+ { "key": "X-Frame-Options", "value": "DENY" },
+ { "key": "X-Content-Type-Options", "value": "nosniff" },
+ { "key": "Strict-Transport-Security", "value": "max-age=63072000" }
+ ]
+ }
+ ]
+ }
+}
+```
+
+## Local Emulators
+
+```bash
+# Start all emulators
+firebase emulators:start
+
+# Start specific emulators
+firebase emulators:start --only auth,firestore,functions
+
+# Export emulator data for persistence
+firebase emulators:export ./emulator-data
+firebase emulators:start --import=./emulator-data
+
+# Emulator UI at http://localhost:4000
+```
+
+```json
+// firebase.json β emulator config
+{
+ "emulators": {
+ "auth": { "port": 9099 },
+ "firestore": { "port": 8080 },
+ "functions": { "port": 5001 },
+ "hosting": { "port": 5000 },
+ "ui": { "enabled": true, "port": 4000 }
+ }
+}
+```
+
+## Environment Configuration
+
+```bash
+# Set environment variables for functions
+firebase functions:config:set stripe.key="sk_live_xxx" app.name="MyApp"
+
+# View config
+firebase functions:config:get
+
+# Use in functions (v1)
+const stripeKey = functions.config().stripe.key;
+
+# For v2 functions, use .env files
+# functions/.env
+STRIPE_KEY=sk_live_xxx
+
+# functions/.env.local (for emulators)
+STRIPE_KEY=sk_test_xxx
+```
+
+## Multi-Environment Setup
+
+```bash
+# Create separate projects for each environment
+firebase use --add # Add staging project alias
+firebase use staging # Switch to staging
+firebase use production
+
+# Deploy to specific project
+firebase deploy --project my-app-staging
+firebase deploy --project my-app-production
+
+# .firebaserc
+{
+ "projects": {
+ "staging": "my-app-staging",
+ "production": "my-app-production"
+ }
+}
+```
+
+## CLI Reference
+
+```bash
+firebase projects:list # List all projects
+firebase deploy # Deploy everything
+firebase deploy --only functions # Deploy only functions
+firebase deploy --only hosting # Deploy only hosting
+firebase deploy --only firestore # Deploy rules + indexes
+firebase functions:log # View function logs
+firebase hosting:channel:create pr-123 # Preview channel
+firebase hosting:channel:delete pr-123
+```
+
+## Security Best Practices
+
+- Write strict Firestore security rules before any other code
+- Separate environments by Firebase project (staging/production)
+- Enable budget alerts and quota monitoring in GCP console
+- Move privileged logic into Cloud Functions (never trust the client)
+- Use App Check to prevent API abuse from non-app clients
+- Enable Firestore audit logging for compliance
+- Review OAuth consent screen settings
+
+## Troubleshooting
+
+| Issue | Solution |
+|-------|---------|
+| Permission denied | Check Firestore rules, verify auth state |
+| Function cold starts | Use min instances (`minInstances: 1`), optimize imports |
+| Emulator won't start | Check port conflicts, run `firebase emulators:start --debug` |
+| Deploy fails | Run `firebase deploy --debug`, check service account permissions |
+| Rules test failing | Use `firebase emulators:exec` to run rules unit tests |
## Related Skills
-- [gcp-cloud-functions](../../cloud-gcp/gcp-cloud-functions/) - Function runtime patterns
-- [vercel-deployments](../vercel-deployments/) - Frontend deployment option
+- [gcp-cloud-functions](../../cloud-gcp/gcp-cloud-functions/) β Function runtime patterns
+- [vercel-deployments](../vercel-deployments/) β Alternative frontend hosting
+- [convex-backend](../convex-backend/) β Alternative managed backend
diff --git a/infrastructure/platforms/vercel-deployments/SKILL.md b/infrastructure/platforms/vercel-deployments/SKILL.md
index 2d92c66..02d043d 100644
--- a/infrastructure/platforms/vercel-deployments/SKILL.md
+++ b/infrastructure/platforms/vercel-deployments/SKILL.md
@@ -1,6 +1,6 @@
---
name: vercel-deployments
-description: Deploy frontend and full-stack apps on Vercel with previews, edge functions, and environment promotion.
+description: Deploy frontend and full-stack apps on Vercel with previews, edge functions, environment promotion, and production guardrails. Use when shipping Next.js, SvelteKit, or static sites with zero-config CI/CD.
license: MIT
metadata:
author: devops-skills
@@ -11,24 +11,270 @@ metadata:
Ship web apps quickly with preview environments and managed edge infrastructure.
-## Core Workflow
+## When to Use This Skill
+
+Use this skill when:
+- Deploying Next.js, SvelteKit, Nuxt, or static sites
+- Setting up preview environments for every PR
+- Configuring edge functions and serverless APIs
+- Managing environment variables across preview/production
+- Setting up custom domains and redirects
+
+## Prerequisites
+
+- Node.js 18+
+- Vercel account (free tier works for personal projects)
+- Git repository (GitHub, GitLab, or Bitbucket)
+
+## Quick Start
```bash
+# Install CLI
npm i -g vercel
+
+# Login and link project
vercel login
vercel link
+
+# Deploy to preview
vercel
+
+# Deploy to production
vercel --prod
+
+# Pull environment variables locally
+vercel env pull .env.local
+```
+
+## Project Configuration
+
+```json
+// vercel.json
+{
+ "framework": "nextjs",
+ "buildCommand": "npm run build",
+ "outputDirectory": ".next",
+ "installCommand": "npm ci",
+ "regions": ["iad1", "sfo1", "cdg1"],
+ "headers": [
+ {
+ "source": "/api/(.*)",
+ "headers": [
+ { "key": "Cache-Control", "value": "no-store" },
+ { "key": "X-Content-Type-Options", "value": "nosniff" }
+ ]
+ },
+ {
+ "source": "/(.*)",
+ "headers": [
+ { "key": "X-Frame-Options", "value": "DENY" },
+ { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" }
+ ]
+ }
+ ],
+ "redirects": [
+ { "source": "/blog/:slug", "destination": "/posts/:slug", "permanent": true }
+ ],
+ "rewrites": [
+ { "source": "/api/v1/:path*", "destination": "https://api.example.com/:path*" }
+ ]
+}
+```
+
+## Environment Variables
+
+```bash
+# Add environment variables
+vercel env add DATABASE_URL production
+vercel env add DATABASE_URL preview
+vercel env add NEXT_PUBLIC_API_URL production
+
+# List all env vars
+vercel env ls
+
+# Pull to local .env.local
+vercel env pull .env.local
+
+# Remove an env var
+vercel env rm SECRET_KEY production
+```
+
+### Environment Separation Pattern
+
+```bash
+# Production β real credentials
+vercel env add DATABASE_URL production <<< "postgresql://prod-host:5432/app"
+vercel env add STRIPE_SECRET_KEY production
+
+# Preview β staging/test credentials
+vercel env add DATABASE_URL preview <<< "postgresql://staging-host:5432/app"
+vercel env add STRIPE_SECRET_KEY preview # Use test mode key
+
+# Development β local values
+vercel env add DATABASE_URL development <<< "postgresql://localhost:5432/app"
+```
+
+## Edge Functions
+
+```typescript
+// app/api/geo/route.ts β Edge API route (Next.js App Router)
+import { NextRequest } from 'next/server';
+
+export const runtime = 'edge';
+
+export function GET(request: NextRequest) {
+ const country = request.geo?.country || 'US';
+ const city = request.geo?.city || 'Unknown';
+
+ return Response.json({
+ country,
+ city,
+ region: request.geo?.region,
+ timestamp: new Date().toISOString(),
+ });
+}
+```
+
+```typescript
+// middleware.ts β Edge middleware for auth/redirects
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+export function middleware(request: NextRequest) {
+ // Block non-US traffic from admin
+ if (request.nextUrl.pathname.startsWith('/admin')) {
+ if (request.geo?.country !== 'US') {
+ return NextResponse.redirect(new URL('/blocked', request.url));
+ }
+ }
+
+ // Add security headers
+ const response = NextResponse.next();
+ response.headers.set('X-Request-Id', crypto.randomUUID());
+ return response;
+}
+
+export const config = {
+ matcher: ['/admin/:path*', '/api/:path*'],
+};
+```
+
+## GitHub Actions Integration
+
+```yaml
+# .github/workflows/preview.yml
+name: Vercel Preview
+on: pull_request
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - run: npm ci
+ - run: npm run lint
+ - run: npm run test
+
+ - name: Deploy to Vercel Preview
+ id: deploy
+ run: |
+ npm i -g vercel
+ URL=$(vercel --token ${{ secrets.VERCEL_TOKEN }} --yes)
+ echo "url=$URL" >> "$GITHUB_OUTPUT"
+
+ - name: Comment PR with preview URL
+ uses: actions/github-script@v7
+ with:
+ script: |
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: `Preview deployed: ${{ steps.deploy.outputs.url }}`
+ });
+```
+
+## CLI Commands Reference
+
+```bash
+# Deployments
+vercel # Deploy to preview
+vercel --prod # Deploy to production
+vercel rollback # Rollback last production deploy
+vercel promote