mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: convex-backend
|
||||
description: Build reactive backends with Convex functions, schema validation, auth integration, and deployment workflows.
|
||||
description: Build reactive backends with Convex functions, schema validation, auth integration, and deployment workflows. Use when building real-time apps with type-safe server functions and automatic caching.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: devops-skills
|
||||
@@ -11,22 +11,311 @@ metadata:
|
||||
|
||||
Use Convex to build type-safe backend logic with realtime data sync.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Building real-time collaborative apps (chat, dashboards, multiplayer)
|
||||
- Need a backend with zero infrastructure management
|
||||
- Want type-safe server functions with automatic caching
|
||||
- Building AI apps that need reactive data (agent status, streaming results)
|
||||
- Prototyping quickly with a managed database + functions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm or pnpm
|
||||
- Convex account (free tier: 1M function calls/month)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Initialize Convex in an existing project
|
||||
npm install convex
|
||||
npx convex dev
|
||||
npx convex deploy
|
||||
npx convex dev # Start local development (syncs with cloud)
|
||||
|
||||
# In a new project
|
||||
npm create convex@latest
|
||||
```
|
||||
|
||||
## Implementation Tips
|
||||
## Schema Definition
|
||||
|
||||
- Define schema and validation before writing functions.
|
||||
- Keep mutations idempotent where possible.
|
||||
- Use auth identity checks in every privileged query/mutation.
|
||||
- Add indexing early for high-read collections.
|
||||
```typescript
|
||||
// convex/schema.ts
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export default defineSchema({
|
||||
users: defineTable({
|
||||
name: v.string(),
|
||||
email: v.string(),
|
||||
role: v.union(v.literal("admin"), v.literal("member")),
|
||||
avatarUrl: v.optional(v.string()),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_email", ["email"])
|
||||
.index("by_role", ["role"]),
|
||||
|
||||
messages: defineTable({
|
||||
userId: v.id("users"),
|
||||
channelId: v.id("channels"),
|
||||
body: v.string(),
|
||||
attachments: v.optional(v.array(v.string())),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_channel", ["channelId", "createdAt"])
|
||||
.index("by_user", ["userId"]),
|
||||
|
||||
channels: defineTable({
|
||||
name: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
isPrivate: v.boolean(),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Queries (Real-Time Reads)
|
||||
|
||||
```typescript
|
||||
// convex/messages.ts
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const listByChannel = query({
|
||||
args: {
|
||||
channelId: v.id("channels"),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const messages = await ctx.db
|
||||
.query("messages")
|
||||
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
|
||||
.order("desc")
|
||||
.take(args.limit ?? 50);
|
||||
|
||||
// Resolve user data for each message
|
||||
return Promise.all(
|
||||
messages.map(async (msg) => {
|
||||
const user = await ctx.db.get(msg.userId);
|
||||
return { ...msg, user: user ? { name: user.name, avatarUrl: user.avatarUrl } : null };
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Mutations (Writes)
|
||||
|
||||
```typescript
|
||||
// convex/messages.ts
|
||||
import { mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const send = mutation({
|
||||
args: {
|
||||
channelId: v.id("channels"),
|
||||
body: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) throw new Error("Not authenticated");
|
||||
|
||||
// Find or create user
|
||||
const user = await ctx.db
|
||||
.query("users")
|
||||
.withIndex("by_email", (q) => q.eq("email", identity.email!))
|
||||
.unique();
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
return await ctx.db.insert("messages", {
|
||||
userId: user._id,
|
||||
channelId: args.channelId,
|
||||
body: args.body,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Actions (External APIs, AI)
|
||||
|
||||
```typescript
|
||||
// convex/ai.ts
|
||||
import { action } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { api } from "./_generated/api";
|
||||
|
||||
export const generateResponse = action({
|
||||
args: { prompt: v.string(), channelId: v.id("channels") },
|
||||
handler: async (ctx, args) => {
|
||||
// Call external AI API
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": process.env.ANTHROPIC_API_KEY!,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "claude-sonnet-4-6",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: args.prompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const aiMessage = data.content[0].text;
|
||||
|
||||
// Save AI response as a message via mutation
|
||||
await ctx.runMutation(api.messages.send, {
|
||||
channelId: args.channelId,
|
||||
body: aiMessage,
|
||||
});
|
||||
|
||||
return aiMessage;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Scheduled Functions (Cron Jobs)
|
||||
|
||||
```typescript
|
||||
// convex/crons.ts
|
||||
import { cronJobs } from "convex/server";
|
||||
import { internal } from "./_generated/api";
|
||||
|
||||
const crons = cronJobs();
|
||||
|
||||
// Run every hour
|
||||
crons.interval("cleanup old messages", { hours: 1 }, internal.maintenance.cleanupOldMessages);
|
||||
|
||||
// Run daily at midnight UTC
|
||||
crons.cron("daily report", "0 0 * * *", internal.reports.generateDailyReport);
|
||||
|
||||
export default crons;
|
||||
```
|
||||
|
||||
## Auth Integration
|
||||
|
||||
```typescript
|
||||
// convex/auth.config.ts
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.AUTH_DOMAIN,
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// React client setup
|
||||
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
||||
import { ClerkProvider, useAuth } from "@clerk/clerk-react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ClerkProvider publishableKey={CLERK_KEY}>
|
||||
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
|
||||
<MyApp />
|
||||
</ConvexProviderWithClerk>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## React Client Usage
|
||||
|
||||
```typescript
|
||||
// src/components/Chat.tsx
|
||||
import { useQuery, useMutation } from "convex/react";
|
||||
import { api } from "../convex/_generated/api";
|
||||
|
||||
export function Chat({ channelId }: { channelId: string }) {
|
||||
// Real-time query — auto-updates when data changes
|
||||
const messages = useQuery(api.messages.listByChannel, { channelId });
|
||||
const sendMessage = useMutation(api.messages.send);
|
||||
|
||||
const handleSend = async (body: string) => {
|
||||
await sendMessage({ channelId, body });
|
||||
};
|
||||
|
||||
if (messages === undefined) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map((msg) => (
|
||||
<div key={msg._id}>
|
||||
<strong>{msg.user?.name}</strong>: {msg.body}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <url> # Promote preview to production
|
||||
|
||||
# Domains
|
||||
vercel domains add example.com
|
||||
vercel domains ls
|
||||
vercel certs ls
|
||||
|
||||
# Logs
|
||||
vercel logs <deployment-url>
|
||||
vercel logs <deployment-url> --follow
|
||||
|
||||
# Project management
|
||||
vercel project ls
|
||||
vercel project rm <name>
|
||||
|
||||
# Inspect deployment
|
||||
vercel inspect <deployment-url>
|
||||
```
|
||||
|
||||
## Production Guardrails
|
||||
|
||||
- Require preview checks before merge.
|
||||
- Separate preview and production environment variables.
|
||||
- Use branch protection with required deployment status.
|
||||
- Monitor function duration and cold start behavior.
|
||||
- Require preview checks before merge (GitHub branch protection)
|
||||
- Separate preview and production environment variables — never share API keys
|
||||
- Use branch protection with required deployment status checks
|
||||
- Monitor function duration and cold start behavior in Vercel Analytics
|
||||
- Set spend limits in Vercel dashboard to prevent cost surprises
|
||||
- Enable Vercel Firewall for DDoS and bot protection
|
||||
- Use `vercel.json` headers for security (CSP, HSTS, X-Frame-Options)
|
||||
|
||||
## Monitoring & Analytics
|
||||
|
||||
```bash
|
||||
# Enable Speed Insights in Next.js
|
||||
npm install @vercel/speed-insights
|
||||
|
||||
# Enable Web Analytics
|
||||
npm install @vercel/analytics
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app/layout.tsx
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
import { SpeedInsights } from '@vercel/speed-insights/next';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|---------|
|
||||
| Build fails | Check `vercel logs`, verify Node.js version in `engines` field |
|
||||
| Env vars missing | Run `vercel env pull`, check variable scope (preview vs production) |
|
||||
| Edge function timeout | Edge has 30s limit; move heavy work to serverless (no `runtime = 'edge'`) |
|
||||
| Cold starts slow | Use edge runtime where possible, reduce bundle size |
|
||||
| Domain not working | Check DNS propagation, verify `vercel domains` configuration |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [github-actions](../../../devops/ci-cd/github-actions/) - Automated deployment gates
|
||||
- [cloudflare-pages](../../cloudflare/cloudflare-pages/) - Alternative edge hosting
|
||||
- [github-actions](../../../devops/ci-cd/github-actions/) — Automated deployment gates
|
||||
- [cloudflare-pages](../../cloudflare/cloudflare-pages/) — Alternative edge hosting
|
||||
- [ssl-tls-management](../../../security/network/ssl-tls-management/) — Custom certificate setup
|
||||
|
||||
Reference in New Issue
Block a user