# Corral — Agent-Native Auth + Payments (Full Reference) > You (the AI agent) can set up complete auth + payments for a project using this file. > No manual wiring required. Read this file, run the commands, done. > > Designed for: OpenClaw, Claude Code, Codex, Cursor, and any AI coding assistant. > > This is the extended reference with full examples. For the concise version: https://docs.llamafarm.dev/corral/llms.txt > GitHub: https://github.com/llama-farm/corral > npm: https://www.npmjs.com/package/create-corral > Version: 0.4.1 ## Quick Start (for agents) > 1. Run: `npx create-corral init` (in the project directory) > 2. Edit `corral.yaml` with the user's desired plans, features, and auth providers > 3. Run: `npx create-corral doctor` > That's it. The app now has auth + payments. Full 9-command workflow (all commands use `npx create-corral `): ```bash npx create-corral analyze --json # 1. detect framework/database npx create-corral init # 2. scaffold all files + install deps npx create-corral add provider google # 3. add OAuth provider npx create-corral add plan pro --price 29 --trial 14 # 4. add paid plan npx create-corral add feature ai_tools --plan pro # 5. gate a feature npx create-corral add meter api_calls --free 500 --pro 10000 # 6. usage meter npx create-corral add page dashboard --gated pro # 7. gated page npx create-corral add device-verify # 8. CLI auth page npx create-corral doctor # 9. verify everything ``` All commands output `--json` with a standard envelope: - Success: `{ "status": "ok", "result": {...}, "next_steps": [...] }` - Error: `{ "status": "error", "error": { "code": "...", "message": "...", "fix": "..." } }` --- ## What Corral Is Corral is an open-source TypeScript library that adds auth, billing, and feature gating to any Node.js app from a single YAML config file. It is: - EMBEDDED (not hosted) — runs inside your app process, same database, same deploy - ONE CONFIG — corral.yaml is the single source of truth for auth + billing + gating - AGENT-NATIVE — --json on every CLI command, auto-generated llms.txt and CORRAL.md - BUILT ON PROVEN LIBS — Better Auth for sessions/OAuth, Stripe for billing Corral is NOT: - A hosted auth service (no Auth0, no Clerk, no per-MAU pricing) - A separate microservice (no Docker required) - A full-stack framework (it mounts inside your existing framework) --- ## Complete corral.yaml Reference ```yaml # ─── App ────────────────────────────────────────────────────────────────────── app: name: HORIZON # Display name url: https://horizon.mil # Production URL (for OAuth callbacks) description: Military intel dashboard # Used in llms.txt # ─── Auth ───────────────────────────────────────────────────────────────────── auth: session_ttl: 7d # Session duration (1h, 7d, 30d, etc.) secret: env:CORRAL_SECRET # 32+ char random string providers: # Google OAuth - google: client_id: env:GOOGLE_CLIENT_ID client_secret: env:GOOGLE_CLIENT_SECRET scopes: [openid, profile, email] # Email magic link - email: magic_link: true otp: false from: noreply@horizon.mil # Email + password # - email: # password: true # GitHub OAuth # - github: # client_id: env:GITHUB_CLIENT_ID # client_secret: env:GITHUB_CLIENT_SECRET # Device auth (CLI tools using this app's auth) - device: client_id: horizon-cli verify_page: /auth/activate # Passkey (WebAuthn) # - passkey # TOTP (2FA) # - totp # SAML SSO # - saml: # metadata_url: https://idp.example.com/metadata # ─── Plans ──────────────────────────────────────────────────────────────────── plans: free: price: 0 description: Basic access. No credit card required. pro: price: 49 # USD cents if < 100, dollars if >= 100 interval: month # month | year description: Full analysis suite. trial_days: 14 inherits: free # inherit free plan features stripe_metadata: tier: pro pro_annual: price: 490 interval: year inherits: pro display_name: Pro (Annual) enterprise: price: custom # no Stripe product; manual invoicing description: Air-gapped deployment, custom SLA. contact: enterprise@horizon.mil # ─── Features ───────────────────────────────────────────────────────────────── features: # Available to all authenticated users on these plans basic_search: plans: [free, pro, enterprise] export_csv: plans: [free, pro, enterprise] # Gated with blur preview for free users ai_analysis: plans: [pro, enterprise] gate: blur # blur | skeleton | block | hide | disabled | redirect description: AI-powered threat analysis # Gated with skeleton for free users real_time_feed: plans: [pro, enterprise] gate: skeleton # Hard block for non-enterprise bulk_export: plans: [enterprise] gate: block # Role-based (not plan-based) admin_console: roles: [admin] gate: hide # ─── Meters ─────────────────────────────────────────────────────────────────── meters: api_calls: plans: free: 500 pro: 10000 enterprise: unlimited window: month # month | week | day | rolling_30d overage: block # block | warn | charge export_rows: plans: free: 1000 pro: 100000 window: month overage: warn # Metered billing (overage charged to Stripe) # ai_tokens: # plans: # pro: 100000 # window: month # overage: charge # overage_price: 0.00001 # stripe_meter_id: env:STRIPE_METER_AI_TOKENS # ─── Database ───────────────────────────────────────────────────────────────── database: adapter: prisma # prisma | drizzle | sqlite | postgres | mysql | turso | d1 url: env:DATABASE_URL # ─── Admin ──────────────────────────────────────────────────────────────────── admin: enabled: true path: /admin roles: [admin] # ─── Webhooks (custom handlers) ─────────────────────────────────────────────── webhooks: invoice.payment_failed: handler: src/handlers/payment-failed.ts customer.subscription.updated: handler: src/handlers/subscription-updated.ts # ─── Email templates ────────────────────────────────────────────────────────── # emails: # magic_link: # subject: "Sign in to HORIZON" # template: emails/magic-link.html # welcome: # subject: "Welcome to HORIZON" # template: emails/welcome.html ``` --- ## Complete CLI Reference with Examples ### corral analyze Read-only analysis of the current project. ```bash # Basic analysis corral analyze # JSON output for agent use corral analyze --json # JSON response: { "status": "ok", "result": { "framework": "nextjs", "router": "app", "typescript": true, "database": "prisma", "existing_auth": null, "existing_billing": null, "corral_installed": false, "package_manager": "npm", "recommended_adapters": ["@corral/core", "@corral/next"] } } ``` ### corral init Scaffold Corral into an existing project. Idempotent — skip existing files. ```bash # Interactive corral init # Non-interactive (agent mode) corral init --yes --json # Force framework/database corral init --framework nextjs --db prisma --yes # Skip billing corral init --no-billing --yes # JSON response: { "status": "ok", "result": { "framework": "nextjs", "files_created": [ "corral.yaml", "src/app/api/auth/[...corral]/route.ts", "src/app/api/billing/[...corral]/route.ts", "src/lib/corral.ts", "CORRAL.md" ], "files_skipped": [], "env_added": ["CORRAL_SECRET", "STRIPE_SECRET_KEY"] }, "next_steps": ["npm install @corral/core @corral/next", "corral validate"] } ``` ### corral add feature Add a feature gate to corral.yaml. Idempotent. ```bash # Add feature gated to pro and enterprise with blur corral add feature ai_analysis --plans pro,enterprise --gate blur # Add role-based feature (not plan-based) corral add feature admin_console --roles admin --gate hide # JSON response: { "status": "ok", "result": { "feature": "ai_analysis", "plans": ["pro", "enterprise"], "gate": "blur", "yaml_updated": true } } ``` ### corral add meter Add a usage meter with per-plan limits. ```bash corral add meter api_calls \ --free 500 \ --pro 10000 \ --enterprise unlimited \ --window month \ --overage block \ --json # JSON response: { "status": "ok", "result": { "meter": "api_calls", "limits": { "free": 500, "pro": 10000, "enterprise": -1 }, "window": "month", "overage": "block" } } ``` ### corral add provider Add an auth provider. Generates OAuth callback route. ```bash corral add provider google --json corral add provider github --scopes "read:user,user:email" --json corral add provider email --magic-link --json corral add provider device --client-id horizon-cli --json ``` ### corral add plan Add a billing plan. Run corral stripe push after. ```bash corral add plan pro --price 49 --interval month --trial 14 --json corral add plan enterprise --price custom --json ``` ### corral stripe push Sync plans to Stripe. Creates Products and Prices. ```bash # Test mode (uses sk_test_...) corral stripe push --json # Preview without applying corral stripe push --dry-run --json # Production corral stripe push --env production --json # JSON response: { "status": "ok", "result": { "created": ["prod_R2xK9mN2aLp", "price_1Ov9mR2xK9mN"], "updated": [], "skipped": ["free plan"], "prices": { "pro": "price_1Ov9mR2xK9mN", "pro_annual": "price_1Ov9mR2xK9mQ" }, "webhook_registered": true } } ``` ### corral validate Full validation against live state. ```bash corral validate --json # JSON response (success): { "status": "ok", "checks": { "config_syntax": "pass", "config_schema": "pass", "env_vars": "pass", "database_connection": "pass", "database_migrations": "pass", "stripe_products": "pass", "stripe_webhooks": "pass", "feature_gates": "pass" }, "warnings": [], "errors": [] } # JSON response (error): { "status": "error", "checks": { ... "stripe_products": "fail" }, "errors": [{ "check": "stripe_products", "code": "STRIPE_PRODUCT_MISSING", "message": "Stripe product for plan 'pro' not found", "fix": "Run: corral stripe push" }] } ``` ### corral seed Create test users in the database. ```bash corral seed --count 3 --json # JSON response: { "status": "ok", "result": { "users": [ { "id": "usr_free_01", "email": "test-free@corral-test.local", "plan": "free" }, { "id": "usr_pro_01", "email": "test-pro@corral-test.local", "plan": "pro" }, { "id": "usr_ent_01", "email": "test-enterprise@corral-test.local", "plan": "enterprise" }, { "id": "usr_admin_01", "email": "test-admin@corral-test.local", "role": "admin", "plan": "pro" } ] } } ``` --- ## Framework Integration Examples ### Next.js App Router (full example) ```typescript // src/lib/corral.ts import { createCorral } from '@corral/core' import { NextAdapter } from '@corral/next' import config from '../../../corral.yaml' export const corral = createCorral({ config, adapter: NextAdapter }) export const { auth, billing, getSession, requireSession, requireFeature, hasFeature, checkLimit, trackUsage, } = corral ``` ```typescript // src/app/api/auth/[...corral]/route.ts import { corral } from '@/lib/corral' export const { GET, POST } = corral.auth.handlers ``` ```typescript // src/app/api/billing/[...corral]/route.ts import { corral } from '@/lib/corral' export const { GET, POST } = corral.billing.handlers ``` ```typescript // src/app/dashboard/page.tsx — Protected server component import { getSession } from '@/lib/corral' import { FeatureGate } from '@corral/next' import { redirect } from 'next/navigation' export default async function DashboardPage() { const session = await getSession() if (!session) redirect('/login') return (

Welcome, {session.user.name}

Plan: {session.plan.id} ({session.plan.status})

{/* gate: blur applied automatically from corral.yaml */} {/* gate: skeleton */}
) } ``` ```typescript // src/app/api/analysis/route.ts — Gated API route import { withFeature, trackUsage, checkLimit } from '@/lib/corral' export const POST = withFeature('ai_analysis', async (req, { session }) => { // Check meter before running const limit = await checkLimit(session.user.id, 'api_calls') if (!limit.ok) { return Response.json({ error: 'meter_exceeded', resetsAt: limit.resetsAt, upgrade_url: '/pricing' }, { status: 429 }) } // Do work const body = await req.json() const result = await runAIAnalysis(body) // Track usage await trackUsage(session.user.id, 'api_calls', 1) return Response.json(result) }) ``` ```typescript // middleware.ts — Route-level protection import { corralMiddleware } from '@corral/next' export function middleware(req) { return corralMiddleware(req, { protectedRoutes: ['/dashboard/:path*'], planRoutes: { '/reports/advanced/:path*': 'pro' }, featureRoutes: { '/api/analysis/:path*': 'ai_analysis' }, roleRoutes: { '/admin/:path*': 'admin' }, loginUrl: '/login', upgradeUrl: '/pricing', }) } export const config = { matcher: ['/dashboard/:path*', '/reports/:path*', '/admin/:path*', '/api/analysis/:path*'] } ``` ### Express (full example) ```typescript import express from 'express' import { createCorral } from '@corral/core' import { ExpressAdapter } from '@corral/express' import config from '../corral.yaml' const app = express() app.use(express.json()) const corral = createCorral({ config, adapter: ExpressAdapter }) // Mount routes app.use('/api/auth', corral.auth.router) app.use('/api/billing', corral.billing.router) // Protected route app.get('/api/intel', corral.requireSession(), async (req, res) => { const { session } = req if (!corral.hasFeature(session, 'ai_analysis')) { return res.status(403).json({ error: 'feature_not_available', feature: 'ai_analysis', required_plan: 'pro', upgrade_url: '/pricing' }) } // Track meter const limit = await corral.checkLimit(session.user.id, 'api_calls') if (!limit.ok) return res.status(429).json({ error: 'meter_exceeded' }) await corral.trackUsage(session.user.id, 'api_calls', 1) res.json({ data: 'classified' }) }) app.listen(3000) ``` ### Python FastAPI (full example) ```python # requirements.txt: corral-py fastapi uvicorn from corral_py import CorralClient from fastapi import Depends, HTTPException, Request, FastAPI app = FastAPI() corral = CorralClient(base_url="https://horizon.mil") async def get_session(request: Request): """Extract and validate session from request.""" token = request.headers.get("Authorization", "").removeprefix("Bearer ") if not token: cookie = request.cookies.get("corral-session") if not cookie: return None token = cookie return await corral.get_session(token) async def require_session(session=Depends(get_session)): if not session: raise HTTPException(status_code=401, detail="Authentication required") return session def require_feature(feature: str): async def dep(session=Depends(require_session)): if feature not in session["features"]: raise HTTPException( status_code=403, detail={ "error": "feature_not_available", "feature": feature, "required_plan": "pro", "upgrade_url": "/pricing" } ) return session return dep @app.get("/api/intel/analysis") async def get_analysis(session=Depends(require_feature("ai_analysis"))): return { "analyst": session["user"]["email"], "plan": session["plan"]["id"], "data": "classified" } ``` --- ## Webhook Handler Example ```typescript // src/handlers/payment-failed.ts import type { WebhookHandler } from '@corral/core' import { sendEmail } from '@/lib/email' import { db } from '@/lib/db' export const handler: WebhookHandler<'invoice.payment_failed'> = async ({ event, // Raw Stripe event user, // Corral user object (or null if not found) subscription // Corral subscription object }) => { const invoice = event.data.object if (user) { // Send dunning email await sendEmail({ to: user.email, subject: 'Payment failed — action required', template: 'payment-failed', data: { amount: invoice.amount_due / 100, currency: invoice.currency.toUpperCase(), retryDate: new Date(invoice.next_payment_attempt * 1000) } }) // Log to database await db.billingEvent.create({ data: { userId: user.id, type: 'payment_failed', amount: invoice.amount_due, invoiceId: invoice.id } }) } } export default handler ``` --- ## Device Auth Flow (CLI Integration) Complete example of a CLI tool using Corral's device auth: ```typescript // horizon-cli/src/auth.ts import { deviceAuth, type DeviceAuthResult } from '@corral/client' import * as fs from 'fs' import * as path from 'path' import * as os from 'os' const CREDENTIALS_PATH = path.join(os.homedir(), '.horizon', 'credentials.json') export async function ensureAuthenticated(): Promise { // Check for cached token if (fs.existsSync(CREDENTIALS_PATH)) { const creds = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8')) if (new Date(creds.expiresAt) > new Date()) { return creds } } // Start device auth flow const auth = new deviceAuth({ baseUrl: 'https://horizon.mil' }) const { userCode, verificationUri, expiresIn } = await auth.start({ clientId: 'horizon-cli', scope: 'openid profile' }) console.log('') console.log(' To authenticate, open your browser and enter the code:') console.log('') console.log(` URL: ${verificationUri}`) console.log(` Code: ${userCode}`) console.log('') console.log(' Waiting for you to approve...') // Poll until approved const token = await auth.poll() // throws if expired // Cache credentials const credentials = { access_token: token.access_token, refresh_token: token.refresh_token, expiresAt: new Date(Date.now() + token.expires_in * 1000).toISOString() } fs.mkdirSync(path.dirname(CREDENTIALS_PATH), { recursive: true }) fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2)) console.log(' ✓ Authenticated!') return credentials } // Usage in CLI command: export async function queryIntel(params: IntelQuery) { const { access_token } = await ensureAuthenticated() const response = await fetch('https://horizon.mil/api/intel', { headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, method: 'POST', body: JSON.stringify(params) }) if (!response.ok) { const error = await response.json() if (error.error === 'upgrade_required') { console.error('This feature requires HORIZON Pro. Visit https://horizon.mil/pricing') process.exit(1) } throw new Error(error.message) } return response.json() } ``` --- ## React Component Examples ### FeatureGate — client side ```tsx import { useFeatureGate, useSession } from '@corral/react' // Simple gate (reads gate mode from corral.yaml) function AIPanel() { return ( ) } // Custom fallback function AIPanel() { return (

Upgrade to Pro to unlock AI Analysis

View Plans → } >
) } // useFeatureGate hook for custom logic function ExportButton() { const { allowed, reason, upgradeUrl } = useFeatureGate('bulk_export') if (!allowed) { return ( ) } return } // Show plan info function PlanBadge() { const { session, loading } = useSession() if (loading) return ... if (!session) return Sign in return ( {session.plan.id} {session.plan.status === 'trialing' && ( (trial ends {session.plan.trialEndsAt?.toLocaleDateString()}) )} ) } ``` --- ## CORRAL.md Format The generated CORRAL.md follows this structure (for agent consumption): ```markdown # Corral in [AppName] Corral v0.4.1 · [Framework] · [Database] ## Auth Routes - Auth handler: [path to auth route] - Billing handler: [path to billing route] - Admin page: [path] - Login page: /api/auth/signin ## Active Features | Feature | Plans | Gate | |---------|-------|------| | ... | ... | ... | ## Active Plans | Plan | Price | Stripe Product | |------|-------|----------------| | ... | ... | ... | ## Common Commands Adding a feature: corral add feature --plans --gate Adding a plan: corral add plan --price corral stripe push Testing locally: corral seed --count 3 npm run dev # Sign in at http://localhost:3000/api/auth/signin ## Required Environment Variables [list of required env vars for this installation] ``` --- ## Error Reference | Code | Message | Fix | |------|---------|-----| | `ENV_MISSING` | Required env var not set | Add to .env.local | | `DB_CONNECTION_FAILED` | Cannot connect to database | Check DATABASE_URL | | `STRIPE_PRODUCT_MISSING` | Stripe product not found | Run `corral stripe push` | | `STRIPE_WEBHOOK_NOT_REGISTERED` | Webhook not registered in Stripe | Run `corral stripe push` | | `FEATURE_NOT_IN_ANY_PLAN` | Feature exists but isn't in any plan | Add plans to feature in corral.yaml | | `PLAN_PRICE_NOT_SYNCED` | Plan price doesn't match Stripe | Run `corral stripe push` | | `SESSION_SECRET_WEAK` | CORRAL_SECRET too short | Use 32+ char random string | | `MIGRATION_PENDING` | Database schema out of date | Run `corral db migrate` | --- ## Upgrade Response Format When a gated API endpoint is called without the right access, Corral returns a structured 403 that your frontend can act on: ```json { "error": "feature_not_available", "feature": "ai_analysis", "reason": "wrong_plan", // wrong_plan | unauthenticated | meter_exceeded | role_required "required_plan": "pro", "current_plan": "free", "upgrade_url": "/pricing", "checkout_url": "/api/billing/checkout?plan=pro" } ``` For meter exceeded (429): ```json { "error": "meter_exceeded", "meter": "api_calls", "limit": 500, "used": 500, "resetsAt": "2024-02-01T00:00:00Z", "upgrade_url": "/pricing", "checkout_url": "/api/billing/checkout?plan=pro" } ``` --- ## Deployment Checklist Before deploying to production, run: ```bash # 1. Validate config corral validate --strict --env production # 2. Push plans to Stripe live mode corral stripe push --env production # 3. Verify Stripe sync corral stripe verify --env production # 4. Check webhook registration # Stripe Dashboard → Webhooks → verify https://yourapp.com/api/billing/webhook # 5. Rotate CORRAL_SECRET if needed # (note: this invalidates all existing sessions) ``` Required production env vars: - CORRAL_SECRET (32+ chars, random) - DATABASE_URL - STRIPE_SECRET_KEY (sk_live_...) - STRIPE_WEBHOOK_SECRET (whsec_...) - GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET (if using Google) - SMTP_URL (if using email magic links) --- ## More - Concise llms.txt: llms.txt - Quickstart: quickstart.html - CLI Reference: cli.html - API Reference: api.html - Framework Guides: frameworks.html - GitHub: https://github.com/llama-farm/corral - License: MIT - Built by LlamaFarm: https://github.com/llama-farm