Skip to main content
OrchestKit v7.85.0 — 107 skills, 37 agents, 188 hooks · Claude Code 2.1.132+
OrchestKit
Skills

Emulate Seed

Generate emulate seed configs for stateful API emulation. Wraps Vercel's emulate tool for GitHub, Vercel, Google OAuth, Slack, Apple Auth, Microsoft Entra, AWS (S3/SQS/IAM), Okta, Clerk, Resend, Stripe, and MongoDB Atlas APIs. Not mocks — full state machines where create-a-PR-and-it-appears-in-the-list, send-an-email-and-retrieve-from-local-inbox. Use when setting up test environments, CI pipelines, integration tests, or offline development.

Reference medium

Auto-activated — this skill loads automatically when Claude detects matching context.

Emulate Seed Configs

Generate and manage seed configs for emulate (Apache-2.0) — Vercel Labs' stateful API emulation tool. Each category has individual rule files in rules/ loaded on-demand.

Paired agent: This skill pairs with the emulate-engineer subagent (subagent_type: "emulate-engineer"). When a task involves generating a full emulate config from scratch, webhook HMAC setup, CI pipeline integration, or parallel-worker port isolation, spawn the agent rather than handling it inline — it has the full 12-emulator service-port matrix and seed-rules in context.

Not mocks. Emulate provides full state machines with cascading deletes, cursor pagination, webhook delivery, and HMAC signature verification. Create a PR via the API and it appears in GET /repos/:owner/:repo/pulls. Delete a repo and its issues, PRs, and webhooks cascade-delete.

New in 2026-04 (emulate 0.4.x)

  • Modular @emulators/* packages — each service is its own package (@emulators/github, @emulators/stripe, etc.); top-level emulate re-exports createEmulator and the CLI.
  • 4 new services (12 total): mongoatlas:4007, okta:4008, resend:4009, stripe:4010 with drop-in seed YAML blocks.
  • Resend local inboxGET http://localhost:4009/inbox returns captured emails for assertions without hitting a real provider.
  • Stripe hosted checkout — real session redirect flow + checkout.session.completed/expired webhook delivery, suitable for E2E payment tests.
  • MongoDB Atlas — Admin API v2 (projects/clusters/DB users) + Data API v1 with full CRUD + aggregate.
  • Okta OIDC — full discovery, JWKS, authorize/token/userinfo/revoke/introspect plus Users/Groups/Apps CRUD.
  • Entra / Apple / Slack expansions (v0.4.0) — PKCE + refresh rotation (Entra), RS256 JWKS (Apple), OAuth v2 consent UI (Slack).
  • @emulators/adapter-next — catch-all Next.js route handler runs emulators on the same origin as the app; fixes OAuth callback URL drift on Vercel preview deploys.

Auto-Discovery (M125 #4)

scripts/auto-discover.sh scans the project's package.json, matches deps against references/dep-to-emulator-map.json, and either reports the matches or writes emulate.config.yaml. Three modes:

ModeBehavior
(default)Report matched deps + emulator union on stderr; do not write
--jsonEmit machine-readable JSON instead of human report
--applyWrite emulate.config.yaml (refuses to overwrite without --force)
$ bash scripts/auto-discover.sh
/ork:emulate-seed --auto scanning /path/to/package.json

Detected:
  @octokit/rest  github · Any GitHub API client
  next-auth  google-oauth, apple-auth, microsoft-entra · Default OAuth providers
  stripe  stripe
  @vercel/blob  aws · @vercel/blob is S3-compatible

Union: apple-auth, aws, github, google-oauth, microsoft-entra, stripe

$ bash scripts/auto-discover.sh --apply

 Wrote /path/to/emulate.config.yaml with 6 service(s)

Multi-emulator deps default to all reasonable providers; the user prunes the YAML afterwards. Unmapped deps are silently skipped — extending coverage is a docs PR (edit references/dep-to-emulator-map.json), not a code change.

/ork:dev reads the resulting emulate.config.yaml at boot — see src/skills/dev/scripts/boot.sh.

Quick Reference

CategoryRulesImpactWhen to Use
Seed Config1HIGHSetting up emulate.config.yaml for test environments
Service Selection1MEDIUMChoosing GitHub/Vercel/Google for your tests
Webhook Setup1MEDIUMTesting webhook delivery with HMAC verification
Parallel CI1HIGHRunning tests in parallel without port collisions
Auth Tokens1MEDIUMSeeding tokens mapped to emulated users

Total: 5 rules across 5 categories

Quick Start

# Install (packages published under @emulators/* scope)
npm install --save-dev emulate

# Start all services
npx emulate

# Start specific services with seed data
npx emulate --service github,stripe --seed ./emulate.config.yaml

# Generate a starter config
npx emulate init --service github

Services (0.5.0 — 13 emulators)

New in 0.5.0 (Apr 2026): Clerk emulator (auth/sessions), portless integration (embedded emulators without dedicated ports), Google OAuth hd claim support, Stripe Checkout + Resend magic link examples, AWS S3 emulator now matches the official SDK wire format. Backwards-compatible.

ServiceDefault PortCoverage
Vercel:4000Projects, deployments, domains, env vars, teams
GitHub:4001Repos, PRs, issues, comments, reviews, Actions, webhooks, orgs, teams
Google OAuth:4002OAuth 2.0 authorize, token exchange, userinfo
Slack:4003Chat, conversations, users, reactions, OAuth v2 with consent UI
Apple Auth:4004Sign in with Apple — OIDC discovery, JWKS (RS256), auth flow, token exchange
Microsoft Entra:4005OAuth 2.0/OIDC v2.0, authorization code + PKCE, refresh token rotation, v1 token endpoint, Graph /users/\{id\}
AWS:4006S3 buckets, SQS queues, IAM users/roles, STS identity
MongoDB Atlas (0.4+):4007Admin API v2 (projects, clusters, DB users) + Data API v1 (full CRUD + aggregate)
Okta (0.4+):4008OIDC discovery, JWKS, authorize/token/userinfo/revoke/introspect, Users/Groups/Apps CRUD
Resend (0.4+):4009Send + batch (100/req), list/retrieve/cancel, domains, API keys, audiences, contacts, local inbox (GET /inbox)
Stripe (0.4+):4010Customers, payment methods, customer sessions, payment intents, charges, products, prices, hosted checkout session w/ webhook delivery
Clerk(on-demand)Users, sessions, organizations

See references/api-coverage.md for full endpoint lists.

Next.js Adapter (0.4+) — @emulators/adapter-next

Runs emulators on the same origin as your Next.js app via a catch-all route handler. Fixes the OAuth callback URL drift problem on Vercel preview deploys — no more http://localhost:4001 redirect mismatches.

// next.config.js
const { withEmulate } = require('@emulators/adapter-next')
module.exports = withEmulate({ /* your next config */ })

// app/api/[...emulate]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
export const { GET, POST } = createEmulateHandler({
  services: ['github', 'stripe', 'resend'],
  persistence: { /* load(), save() or built-in filePersistence */ },
})

Seed Config Structure

A seed config pre-populates the emulator with tokens, users, repos, and projects so tests start from a known state.

# emulate.config.yaml
tokens:
  dev_token:
    login: yonatangross
    scopes: [repo, workflow, admin:org]
  ci_token:
    login: ci-bot
    scopes: [repo]

github:
  users:
    - login: yonatangross
      name: Yonatan Gross
    - login: ci-bot
      name: CI Bot
  repos:
    - owner: yonatangross
      name: my-project
      private: false
      default_branch: main
      topics: [typescript, testing]

vercel:
  users:
    - username: yonatangross
      email: yonaigross@gmail.com
  projects:
    - name: my-docs
      framework: next

# NEW in 0.4.x — drop-in seed blocks
okta:
  users:
    - login: alice@example.com
      firstName: Alice
      lastName: Smith
  groups: [{ name: Everyone }, { name: Admins }]
  apps: [{ name: My Web App }]
  authorization_servers:
    - name: default
      audiences: ["api://default"]

resend:
  domains: [{ name: example.com }]
  api_keys: [{ name: default }]
  # In tests: GET http://localhost:4009/inbox to assert captured emails

stripe:
  customers:
    - name: Test Customer
      email: customer@example.com
  products: [{ name: Pro Plan }, { name: Starter Plan }]
  prices:
    - { product: Pro Plan, unit_amount: 4900, currency: usd, recurring: { interval: month } }
    - { product: Starter Plan, unit_amount: 1900, currency: usd, recurring: { interval: month } }
  # Webhook delivery fires on checkout.session.completed / expired

mongoatlas:
  projects: [{ name: my-project }]
  clusters: [{ project: my-project, name: my-cluster }]
  database_users: [{ project: my-project, username: app-user }]

See rules/seed-config.md for full schema and best practices.

Programmatic SDK

Service packages live under the @emulators/* scope (e.g., @emulators/github, @emulators/stripe). The programmatic API (createEmulator) is exported from the top-level emulate package.

import { createEmulator } from 'emulate'

const github = await createEmulator({ service: 'github', port: 4001 })
// github.url -> 'http://localhost:4001'

// State is real — create a PR and it appears in the list
const res = await fetch(`${github.url}/repos/org/repo/pulls`, {
  method: 'POST',
  headers: { Authorization: 'Bearer dev_token' },
  body: JSON.stringify({ title: 'Test PR', head: 'feature', base: 'main' })
})

const prs = await fetch(`${github.url}/repos/org/repo/pulls`)
// -> includes the PR we just created

// Cleanup
github.reset()       // Synchronous state wipe
await github.close() // Shut down server

See references/sdk-patterns.md for advanced patterns (multi-service, lifecycle hooks).

Webhook Delivery

Emulate delivers real webhooks with HMAC-SHA256 signatures when state changes:

import crypto from 'crypto'

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

See rules/webhook-setup.md for webhook receiver patterns.

CI Integration

# .github/workflows/test.yml
jobs:
  test:
    steps:
      - uses: actions/checkout@v4
      - name: Start emulate
        run: npx emulate --service github --seed .emulate/ci.yaml &
      - name: Wait for emulate
        run: sleep 2
      - name: Run tests
        run: npm test
        env:
          GITHUB_API_BASE: http://localhost:4001
          VERCEL_API_BASE: http://localhost:4000

Parallel Test Execution

Each test worker gets its own port to avoid race conditions:

// vitest.config.ts
const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')

See rules/parallel-ci.md for full parallel isolation patterns.

Decision Matrix

ToolWhen to UseStateful?Platforms
emulate (FIRST CHOICE)GitHub/Vercel/Google/Slack/Apple/Entra/AWS/Okta/Resend/Stripe/MongoDB testingYESAll 12 services
PactContract verification between servicesNoAny
MSWIn-browser/Node HTTP mockingNoAny
NockNode.js HTTP interceptNoAny
WireMockHTTP stub serverPartialAny

Use emulate when:

  • Testing code that calls GitHub, Vercel, Google, Slack, Apple, Entra, AWS, Okta, Resend, Stripe, or MongoDB Atlas
  • You need state persistence across multiple API calls in a test
  • You want webhook delivery with real HMAC signatures (GitHub, Stripe)
  • You need cascading side-effects (delete repo -> PRs cascade-delete)
  • You need to assert on sent emails without hitting a real provider (Resend local /inbox)
  • You need hosted Stripe checkout sessions with real redirect flow in tests

Use MSW/Nock when:

  • Mocking arbitrary HTTP APIs not covered by emulate
  • You need in-browser interception (MSW)
  • Tests only need single request/response pairs
  • testing-integration — Integration test patterns (emulate as first choice for API tests)
  • testing-e2e — End-to-end test patterns with emulated backends
  • testing-unit — Unit test patterns (use emulate for API-dependent units)
  • security-patterns — Auth token patterns (emulate token seeding)

CLI Reference

See references/cli-reference.md for all CLI flags and commands.

SDK Patterns

See references/sdk-patterns.md for programmatic createEmulate() usage.


Rules (5)

Auth Tokens: Token Seeding and Credential Hygiene — MEDIUM

Auth Token Seeding

Emulate maps token strings to emulated users with configurable scopes. Tests use these seeded token names as Bearer tokens — no real credentials involved.

Token Configuration

# emulate.config.yaml
tokens:
  admin_token:
    login: admin-user
    scopes: [repo, admin:org, workflow, delete_repo]
  dev_token:
    login: dev-user
    scopes: [repo, workflow]
  readonly_token:
    login: viewer
    scopes: [repo:read]
  ci_token:
    login: ci-bot
    scopes: [repo, actions]

Using Seeded Tokens

// The token name IS the Bearer value
const adminRes = await fetch(`${GITHUB_API_BASE}/orgs/my-org/repos`, {
  method: 'POST',
  headers: { Authorization: 'Bearer admin_token' },
  body: JSON.stringify({ name: 'new-repo' })
})
// -> 201 Created (admin_token has admin:org scope)

const readonlyRes = await fetch(`${GITHUB_API_BASE}/orgs/my-org/repos`, {
  method: 'POST',
  headers: { Authorization: 'Bearer readonly_token' },
  body: JSON.stringify({ name: 'new-repo' })
})
// -> 403 Forbidden (readonly_token lacks admin:org scope)

Incorrect — using real tokens in test configs:

# BAD: real GitHub PAT in test config — leaks if committed
tokens:
  my_token:
    login: yonatangross
    value: ghp_abc123realtoken456  # NEVER put real tokens here
// BAD: hardcoded real token in test file
headers: { Authorization: 'Bearer ghp_abc123realtoken456' }

Correct — descriptive seeded token names:

# GOOD: token name is the bearer value, no real credentials
tokens:
  test_admin:
    login: admin-user
    scopes: [repo, admin:org]
// GOOD: seeded token name as bearer value
headers: { Authorization: 'Bearer test_admin' }

Permission Testing Pattern

describe('permission checks', () => {
  it('admin can delete repos', async () => {
    const res = await fetch(`${GITHUB_API_BASE}/repos/org/repo`, {
      method: 'DELETE',
      headers: { Authorization: 'Bearer admin_token' }
    })
    expect(res.status).toBe(204)
  })

  it('readonly user cannot delete repos', async () => {
    const res = await fetch(`${GITHUB_API_BASE}/repos/org/repo`, {
      method: 'DELETE',
      headers: { Authorization: 'Bearer readonly_token' }
    })
    expect(res.status).toBe(403)
  })
})

Key rules:

  • Token names in the config are the literal Bearer strings used in API requests
  • Never put real GitHub PATs, Vercel tokens, or Google credentials in seed configs
  • Use descriptive token names: admin_token, ci_token, readonly_token
  • Map each token to a user login defined in the same config
  • Test permission boundaries by creating tokens with different scope sets
  • Keep token configs in committed files for reproducibility — they contain no secrets

Reference: rules/seed-config.md

Parallel CI: Per-Worker Port Isolation — HIGH

Parallel CI Port Isolation

When running tests in parallel (Vitest, Jest workers, CI matrix), each worker needs its own emulator instance on a unique port to prevent shared state and race conditions.

Per-Worker Port Offset

// vitest.setup.ts — each worker gets a unique port
import { createEmulate } from '@emulators/emulate'
import type { Emulator } from '@emulators/emulate'

let github: Emulator

const BASE_PORT = 4001
const workerPort = BASE_PORT + parseInt(process.env.VITEST_WORKER_ID || '0')

beforeAll(async () => {
  github = await createEmulate({
    service: 'github',
    port: workerPort,
    seed: './emulate.config.yaml'
  })
  process.env.GITHUB_API_BASE = github.url
})

afterAll(async () => {
  await github.close()
})

beforeEach(() => {
  github.reset() // Wipe state between tests, keep server running
})

Jest Worker Isolation

// jest.setup.ts
const workerPort = 4001 + parseInt(process.env.JEST_WORKER_ID || '0')

Incorrect — all workers hitting the same port:

// BAD: shared port causes race conditions
const github = await createEmulate({ service: 'github', port: 4001 })

// Worker 1 creates a PR, Worker 2 sees it — non-deterministic

Correct — per-worker port isolation:

// GOOD: each worker has isolated state
const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')
const github = await createEmulate({ service: 'github', port: workerPort })

// Worker 1 on :4002, Worker 2 on :4003 — fully isolated

CI Matrix Isolation

# .github/workflows/test.yml
jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - name: Start emulate
        run: |
          PORT_OFFSET=$((4001 + ${{ matrix.shard }} * 100))
          npx emulate --service github --port $PORT_OFFSET --seed .emulate/ci.yaml &
          echo "GITHUB_API_BASE=http://localhost:$PORT_OFFSET" >> $GITHUB_ENV
      - name: Wait for emulate
        run: sleep 2
      - name: Run tests
        run: npm test -- --shard=${{ matrix.shard }}/4

Key rules:

  • Compute port as BASE_PORT + worker_id to guarantee uniqueness per worker
  • Create a fresh emulator instance per worker in beforeAll, close in afterAll
  • Use github.reset() in beforeEach to wipe state between tests within a worker
  • In CI matrix builds, use shard index with a multiplier (e.g., * 100) to avoid port overlap between shards
  • Never rely on a single shared emulator instance for parallel test execution
  • Always set GITHUB_API_BASE per worker so test code uses the correct port

Reference: references/sdk-patterns.md

Seed Config: emulate.config.yaml Structure — HIGH

Seed Config Structure

The emulate.config.yaml file pre-populates the emulator with tokens, users, repos, and projects so every test starts from a known, reproducible state.

Config Sections

# emulate.config.yaml — full structure
tokens:
  dev_token:
    login: dev-user        # Maps this token string to a user
    scopes: [repo, workflow, admin:org]
  read_only_token:
    login: reader
    scopes: [repo:read]

github:
  users:
    - login: dev-user
      name: Developer
    - login: reader
      name: Read-Only User
  orgs:
    - login: my-org
      name: My Organization
  repos:
    - owner: my-org
      name: backend
      private: false
      default_branch: main
      topics: [api, typescript]
    - owner: dev-user
      name: side-project
      private: true
      default_branch: main

vercel:
  users:
    - username: dev-user
      email: dev@example.com
  projects:
    - name: frontend
      framework: next
    - name: docs
      framework: astro

google:
  users:
    - email: dev@example.com
      name: Developer

Incorrect — hardcoding tokens in test files:

// BAD: tokens scattered across test files, no shared state
const res = await fetch('http://localhost:4001/repos/org/repo', {
  headers: { Authorization: 'Bearer ghp_realtoken123' }
})

Correct — centralized seed config:

# .emulate/test.config.yaml
tokens:
  test_admin:
    login: admin-user
    scopes: [repo, admin:org]
// Tests reference seeded token names
const res = await fetch(`${GITHUB_API_BASE}/repos/org/repo`, {
  headers: { Authorization: 'Bearer test_admin' }
})

Key rules:

  • Place configs in .emulate/ directory or project root
  • One config per environment: ci.config.yaml, dev.config.yaml, test.config.yaml
  • Token names are the literal Bearer strings used in requests — keep them descriptive
  • Every token must map to a user defined in the same config's users section
  • Add .emulate/ to .gitignore only if it contains environment-specific overrides
  • Commit shared base configs (e.g., emulate.config.yaml) to the repo for reproducibility
  • Use npx emulate init --service github to generate a starter config

Reference: references/cli-reference.md

Service Selection: GitHub, Vercel, and Google — MEDIUM

Service Selection

Choose the right emulate service based on which APIs your code interacts with. Use emulate as the first choice whenever the target API is covered.

Service Defaults

ServiceFlagPortUse When
GitHub--service github:4001Repos, PRs, issues, reviews, Actions, webhooks, orgs
Vercel--service vercel:4000Projects, deployments, domains, env vars, teams
Google OAuth--service google:4002OAuth 2.0 flows, token exchange, userinfo

Multi-Service

# Start GitHub + Vercel together
npx emulate --service github,vercel --seed ./emulate.config.yaml

# Custom ports
npx emulate --service github --port 5001

Incorrect — writing manual GitHub API mocks when emulate covers it:

// BAD: hand-rolled mock that doesn't maintain state
const mockListPRs = jest.fn().mockResolvedValue([])
const mockCreatePR = jest.fn().mockResolvedValue({ number: 1 })
// After createPR, listPRs still returns [] — not stateful

Correct — use emulate for stateful GitHub API testing:

import { createEmulate } from '@emulators/emulate'

const github = await createEmulate({ service: 'github', port: 4001 })

// Create PR via API
await fetch(`${github.url}/repos/org/repo/pulls`, {
  method: 'POST',
  headers: { Authorization: 'Bearer dev_token' },
  body: JSON.stringify({ title: 'Fix bug', head: 'fix', base: 'main' })
})

// PR now appears in list — state is real
const prs = await (await fetch(`${github.url}/repos/org/repo/pulls`)).json()
expect(prs).toHaveLength(1)
expect(prs[0].title).toBe('Fix bug')

await github.close()

Key rules:

  • Use emulate --service github whenever testing GitHub API interactions — it covers repos, PRs, issues, comments, reviews, Actions, webhooks, orgs, and teams
  • Use emulate --service vercel for Vercel platform API testing — projects, deployments, domains, env vars
  • Use emulate --service google for Google OAuth flows — authorize, token exchange, userinfo
  • Combine services with comma separation: --service github,vercel
  • Fall back to MSW/Nock only for APIs emulate does not cover
  • Custom ports via --port when defaults conflict with existing services

Reference: references/api-coverage.md

Webhook Setup: HMAC Delivery and Verification — MEDIUM

Webhook Setup

Emulate delivers real webhooks with HMAC-SHA256 signatures when state changes occur (PR created, issue opened, deployment completed). Configure a webhook receiver in your tests to verify delivery and signatures.

Webhook Receiver Pattern

import http from 'http'
import crypto from 'crypto'

const WEBHOOK_SECRET = 'test-webhook-secret'
const receivedEvents: Array<{ event: string; payload: object }> = []

const webhookServer = http.createServer((req, res) => {
  let body = ''
  req.on('data', chunk => { body += chunk })
  req.on('end', () => {
    // Verify HMAC signature
    const signature = req.headers['x-hub-signature-256'] as string
    const expected = 'sha256=' + crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(body)
      .digest('hex')

    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      res.writeHead(401)
      res.end('Invalid signature')
      return
    }

    receivedEvents.push({
      event: req.headers['x-github-event'] as string,
      payload: JSON.parse(body)
    })
    res.writeHead(200)
    res.end('OK')
  })
})

webhookServer.listen(9876)

Incorrect — skipping signature verification in tests:

// BAD: no signature check — production HMAC bugs go undetected
webhookServer.on('request', (req, res) => {
  let body = ''
  req.on('data', chunk => { body += chunk })
  req.on('end', () => {
    receivedEvents.push(JSON.parse(body))  // Just trust it
    res.writeHead(200).end()
  })
})

Correct — verify HMAC even in tests:

// GOOD: same verification logic as production
const signature = req.headers['x-hub-signature-256'] as string
const expected = 'sha256=' + crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(body)
  .digest('hex')

if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
  res.writeHead(401).end('Invalid signature')
  return
}

Key rules:

  • Always verify HMAC-SHA256 signatures in test webhook receivers — mirrors production behavior
  • Use crypto.timingSafeEqual for constant-time comparison to prevent timing attacks
  • Store the webhook secret in the seed config and share it with the receiver
  • Check the x-github-event header to route different event types
  • Clean up the webhook server in afterAll to prevent port leaks
  • Emulate delivers webhooks on state mutations (create, update, delete) — not on reads

Reference: references/sdk-patterns.md


References (7)

Api Coverage

API Coverage

Full list of supported API endpoints per emulate service.

GitHub API (:4001)

Repositories

  • GET /repos/:owner/:repo — Get repository
  • POST /user/repos — Create user repository
  • POST /orgs/:org/repos — Create org repository
  • PATCH /repos/:owner/:repo — Update repository
  • DELETE /repos/:owner/:repo — Delete repository (cascading: PRs, issues, webhooks)
  • GET /repos/:owner/:repo/topics — List topics
  • PUT /repos/:owner/:repo/topics — Replace topics

Pull Requests

  • GET /repos/:owner/:repo/pulls — List PRs (cursor pagination)
  • POST /repos/:owner/:repo/pulls — Create PR
  • GET /repos/:owner/:repo/pulls/:number — Get PR
  • PATCH /repos/:owner/:repo/pulls/:number — Update PR
  • PUT /repos/:owner/:repo/pulls/:number/merge — Merge PR
  • GET /repos/:owner/:repo/pulls/:number/reviews — List reviews
  • POST /repos/:owner/:repo/pulls/:number/reviews — Create review
  • GET /repos/:owner/:repo/pulls/:number/comments — List review comments
  • POST /repos/:owner/:repo/pulls/:number/comments — Create review comment

Issues

  • GET /repos/:owner/:repo/issues — List issues
  • POST /repos/:owner/:repo/issues — Create issue
  • GET /repos/:owner/:repo/issues/:number — Get issue
  • PATCH /repos/:owner/:repo/issues/:number — Update issue
  • GET /repos/:owner/:repo/issues/:number/comments — List comments
  • POST /repos/:owner/:repo/issues/:number/comments — Create comment

Actions / Workflows

  • GET /repos/:owner/:repo/actions/workflows — List workflows
  • POST /repos/:owner/:repo/actions/workflows/:id/dispatches — Trigger workflow
  • GET /repos/:owner/:repo/actions/runs — List workflow runs
  • GET /repos/:owner/:repo/actions/runs/:id — Get run
  • GET /repos/:owner/:repo/actions/runs/:id/jobs — List jobs

Webhooks

  • GET /repos/:owner/:repo/hooks — List webhooks
  • POST /repos/:owner/:repo/hooks — Create webhook
  • PATCH /repos/:owner/:repo/hooks/:id — Update webhook
  • DELETE /repos/:owner/:repo/hooks/:id — Delete webhook

Organizations & Teams

  • GET /orgs/:org — Get organization
  • GET /orgs/:org/repos — List org repos
  • GET /orgs/:org/teams — List teams
  • POST /orgs/:org/teams — Create team
  • GET /orgs/:org/members — List members

Users

  • GET /user — Authenticated user
  • GET /users/:username — Get user
  • GET /users/:username/repos — List user repos

Vercel API (:4000)

Projects

  • GET /v9/projects — List projects
  • POST /v9/projects — Create project
  • GET /v9/projects/:id — Get project
  • PATCH /v9/projects/:id — Update project
  • DELETE /v9/projects/:id — Delete project

Deployments

  • GET /v6/deployments — List deployments
  • POST /v13/deployments — Create deployment
  • GET /v13/deployments/:id — Get deployment
  • DELETE /v13/deployments/:id — Cancel deployment

Domains

  • GET /v5/domains — List domains
  • POST /v5/domains — Add domain
  • DELETE /v5/domains/:name — Remove domain

Environment Variables

  • GET /v9/projects/:id/env — List env vars
  • POST /v9/projects/:id/env — Create env var
  • PATCH /v9/projects/:id/env/:envId — Update env var
  • DELETE /v9/projects/:id/env/:envId — Delete env var

Teams

  • GET /v2/teams — List teams
  • POST /v1/teams — Create team
  • GET /v2/teams/:id — Get team

Google OAuth (:4002)

OAuth 2.0

  • GET /o/oauth2/v2/auth — Authorization endpoint
  • POST /oauth2/v4/token — Token exchange
  • GET /oauth2/v2/userinfo — User info
  • POST /oauth2/revoke — Token revocation

Slack Web API (:4003)

Chat & Conversations

  • POST /api/chat.postMessage — Send message
  • GET /api/conversations.list — List conversations
  • GET /api/conversations.history — Get messages in channel
  • POST /api/reactions.add — Add emoji reaction
  • GET /api/users.list — List workspace users

OAuth

  • GET /oauth/v2/authorize — OAuth v2 consent UI
  • POST /api/oauth.v2.access — Token exchange

Apple Authentication (:4004)

  • GET /.well-known/openid-configuration — OIDC discovery
  • GET /auth/keys — JWKS endpoint (RS256)
  • GET /auth/authorize — Authorization flow
  • POST /auth/token — Token exchange
  • POST /auth/revoke — Token revocation

Microsoft Entra ID (:4005)

  • GET /\{tenant\}/v2.0/.well-known/openid-configuration — OIDC discovery
  • GET /\{tenant\}/oauth2/v2.0/authorize — Authorization code + PKCE
  • POST /\{tenant\}/oauth2/v2.0/token — Token exchange with refresh rotation
  • GET /\{tenant\}/oauth2/v2.0/logout — Logout endpoint

AWS (:4006)

S3

  • PUT /\{bucket\} — Create bucket
  • GET / — List buckets
  • PUT /\{bucket\}/\{key\} — Put object
  • GET /\{bucket\}/\{key\} — Get object
  • DELETE /\{bucket\}/\{key\} — Delete object

SQS

  • POST / — CreateQueue, SendMessage, ReceiveMessage, DeleteMessage

IAM & STS

  • POST / — CreateUser, CreateRole, GetCallerIdentity, AssumeRole

Stateful Behaviors

All services maintain full state:

  • Cascading deletes: Delete a repo and its PRs, issues, and webhooks are removed
  • Cursor pagination: List endpoints support ?per_page=N&page=N and Link headers
  • Auto-incrementing IDs: PRs, issues, comments get sequential IDs
  • Webhook delivery: State mutations trigger webhook POST to configured URLs with HMAC signatures
  • Scope enforcement: Token scopes are checked — insufficient scopes return 403

Cli Reference

CLI Reference

All emulate CLI commands and flags.

Commands

emulate (default)

Start emulate services.

# Start all services with defaults
npx emulate

# Specific services
npx emulate --service github
npx emulate --service github,vercel
npx emulate --service github,vercel,google

# With seed data
npx emulate --seed ./emulate.config.yaml
npx emulate --service github --seed .emulate/dev.yaml

# Custom port (overrides default for first service)
npx emulate --service github --port 5001

# Combine flags
npx emulate --service github,vercel --port 3000 --seed ./config.yaml

emulate init

Generate a starter seed config.

# Generate config for GitHub
npx emulate init --service github

# Generate config for all services
npx emulate init

# Output to specific file
npx emulate init --service github > .emulate/github.yaml

emulate list

List available services and their default ports.

npx emulate list
# github   :4001
# vercel   :4000
# google   :4002

Flags

FlagShortDefaultDescription
--service-sallComma-separated services: github, vercel, google
--port-pper-serviceOverride default port for the first listed service
--seednonePath to YAML seed config file
--help-hShow help
--version-vShow version

Port Defaults

When no --port is specified:

ServiceDefault Port
Vercel4000
GitHub4001
Google4002

When --port is specified with multiple services, ports increment from the given base:

npx emulate --service github,vercel --port 3000
# github -> :3000
# vercel -> :3001

Environment Variables

Set these in your test runner or CI to redirect API calls to the emulator:

VariableExampleSDK/Tool
GITHUB_API_BASEhttp://localhost:4001Octokit, gh CLI
VERCEL_API_BASEhttp://localhost:4000Vercel SDK
GOOGLE_OAUTH_BASEhttp://localhost:4002Google Auth libraries

Exit Codes

CodeMeaning
0Clean shutdown
1Config parse error or port conflict
130Interrupted (Ctrl+C)

Sdk Patterns

SDK Patterns

Programmatic usage of emulate via the createEmulate() API (v0.3.0+, @emulators/* scope).

Basic Usage

import { createEmulate } from '@emulators/emulate'

const github = await createEmulate({
  service: 'github',
  port: 4001,
  seed: './emulate.config.yaml'  // Optional seed file
})

console.log(github.url) // 'http://localhost:4001'

// Use the emulated API
const res = await fetch(`${github.url}/repos/org/repo`)
const repo = await res.json()

// Cleanup
github.reset()       // Synchronous — wipes all state, keeps server running
await github.close() // Async — shuts down server and frees port

Multi-Service Setup

import { createEmulate } from '@emulators/emulate'

const [github, vercel] = await Promise.all([
  createEmulate({ service: 'github', port: 4001, seed: './config.yaml' }),
  createEmulate({ service: 'vercel', port: 4000, seed: './config.yaml' }),
])

// Both share the same seed config — tokens, users, projects
const ghRes = await fetch(`${github.url}/repos/org/repo`)
const vcRes = await fetch(`${vercel.url}/v9/projects`)

// Cleanup both
await Promise.all([github.close(), vercel.close()])

Test Framework Integration

Vitest

// vitest.setup.ts
import { createEmulate, type Emulator } from '@emulators/emulate'

let github: Emulator

beforeAll(async () => {
  const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')
  github = await createEmulate({
    service: 'github',
    port: workerPort,
    seed: '.emulate/test.yaml'
  })
  process.env.GITHUB_API_BASE = github.url
})

afterAll(async () => {
  await github.close()
})

beforeEach(() => {
  github.reset() // Fresh state per test
})

Jest

// jest.setup.ts
import { createEmulate, type Emulator } from '@emulators/emulate'

let github: Emulator

beforeAll(async () => {
  const workerPort = 4001 + parseInt(process.env.JEST_WORKER_ID || '0')
  github = await createEmulate({
    service: 'github',
    port: workerPort,
    seed: '.emulate/test.yaml'
  })
  process.env.GITHUB_API_BASE = github.url
})

afterAll(async () => {
  await github.close()
})

beforeEach(() => {
  github.reset()
})

Emulator API

createEmulate(options)

Creates and starts an emulator instance.

interface EmulatorOptions {
  service: 'github' | 'vercel' | 'google'
  port?: number      // Default: 4001 (github), 4000 (vercel), 4002 (google)
  seed?: string      // Path to YAML seed config
}

const emulator: Emulator = await createEmulate(options)

emulator.url

The base URL of the running emulator.

github.url // 'http://localhost:4001'

emulator.reset()

Synchronously wipes all state and re-applies the seed config. The server stays running — useful for resetting between tests without the overhead of restarting.

github.reset() // Instant — no await needed

emulator.close()

Asynchronously shuts down the server and frees the port. Call in afterAll.

await github.close()

URL Patterns

When using emulate with existing SDKs, set the base URL:

Octokit

import { Octokit } from '@octokit/rest'

const octokit = new Octokit({
  baseUrl: process.env.GITHUB_API_BASE || 'https://api.github.com',
  auth: 'dev_token'  // Seeded token name
})

const { data: repos } = await octokit.repos.listForOrg({ org: 'my-org' })

Vercel SDK

import { Vercel } from '@vercel/sdk'

const vercel = new Vercel({
  baseUrl: process.env.VERCEL_API_BASE || 'https://api.vercel.com',
  bearerToken: 'dev_token'
})

fetch

const base = process.env.GITHUB_API_BASE || 'https://api.github.com'

const res = await fetch(`${base}/repos/org/repo/pulls`, {
  headers: { Authorization: 'Bearer dev_token' }
})

State Lifecycle

createEmulate() -> seed applied -> tests run -> reset() -> tests run -> close()
                     ^                            ^
                     |                            |
                     Initial state                State wiped, seed re-applied
  • createEmulate() — Starts server, applies seed config
  • Tests run — State accumulates (created PRs, issues, etc.)
  • reset() — Wipes state, re-applies seed — server stays up
  • close() — Shuts down server, frees port

Upstream Github

<!-- SYNCED from vercel-labs/emulate (skills/github/SKILL.md) --> <!-- Hash: 635cb8ee698f5fe8b6fb4fafa9d5aaf0d9cd697c71fd980d1b56d34c328bda00 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

GitHub API Emulator

Fully stateful GitHub REST API emulation. Creates, updates, and deletes persist in memory and affect related entities.

Start

# GitHub only
npx emulate --service github

# Default port
# http://localhost:4001

Or programmatically:

import { createEmulator } from 'emulate'

const github = await createEmulator({ service: 'github', port: 4001 })
// github.url === 'http://localhost:4001'

Auth

Pass tokens as Authorization: Bearer &lt;token&gt; or Authorization: token &lt;token&gt;.

curl http://localhost:4001/user \
  -H "Authorization: Bearer gho_test_token_admin"

Public repo endpoints work without auth. Private repos and write operations require a valid token. When no token is provided, requests fall back to the first seeded user.

GitHub App JWT

Configure apps in the seed config with a private key. Sign a JWT with \{ iss: "&lt;app_id&gt;" \} using RS256. The emulator verifies the signature and resolves the app.

github:
  apps:
    - app_id: 12345
      slug: my-github-app
      name: My GitHub App
      private_key: |
        -----BEGIN RSA PRIVATE KEY-----
        ...
        -----END RSA PRIVATE KEY-----
      permissions:
        contents: read
        issues: write
      events: [push, pull_request]
      installations:
        - installation_id: 100
          account: my-org
          repository_selection: all

Pointing Your App at the Emulator

Environment Variable

GITHUB_EMULATOR_URL=http://localhost:4001

Octokit

import { Octokit } from '@octokit/rest'

const octokit = new Octokit({
  baseUrl: process.env.GITHUB_EMULATOR_URL ?? 'https://api.github.com',
  auth: 'gho_test_token_admin',
})

OAuth URL Mapping

Real GitHub URLEmulator URL
https://github.com/login/oauth/authorize$GITHUB_EMULATOR_URL/login/oauth/authorize
https://github.com/login/oauth/access_token$GITHUB_EMULATOR_URL/login/oauth/access_token
https://api.github.com/user$GITHUB_EMULATOR_URL/user

Auth.js / NextAuth.js

import GitHub from '@auth/core/providers/github'

GitHub({
  clientId: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  authorization: {
    url: `${process.env.GITHUB_EMULATOR_URL}/login/oauth/authorize`,
  },
  token: {
    url: `${process.env.GITHUB_EMULATOR_URL}/login/oauth/access_token`,
  },
  userinfo: {
    url: `${process.env.GITHUB_EMULATOR_URL}/user`,
  },
})

Seed Config

tokens:
  gho_test_token_admin:
    login: admin
    scopes: [repo, user, admin:org, admin:repo_hook]

github:
  users:
    - login: octocat
      name: The Octocat
      email: octocat@github.com
      bio: I am the Octocat
      company: GitHub
      location: San Francisco
  orgs:
    - login: my-org
      name: My Organization
      description: A test organization
  repos:
    - owner: octocat
      name: hello-world
      description: My first repository
      language: JavaScript
      topics: [hello, world]
      auto_init: true
  oauth_apps:
    - client_id: Iv1.abc123
      client_secret: secret_abc123
      name: My Web App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github

Pagination

All list endpoints support page and per_page query params with Link headers:

curl "http://localhost:4001/repos/octocat/hello-world/issues?page=1&per_page=10" \
  -H "Authorization: Bearer gho_test_token_admin"

API Endpoints

Users

# Authenticated user
curl http://localhost:4001/user -H "Authorization: Bearer $TOKEN"

# Update profile
curl -X PATCH http://localhost:4001/user \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"bio": "Hello!"}'

# Get user by username
curl http://localhost:4001/users/octocat

# List users
curl http://localhost:4001/users

# User repos / orgs / followers / following
curl http://localhost:4001/users/octocat/repos
curl http://localhost:4001/users/octocat/orgs
curl http://localhost:4001/users/octocat/followers
curl http://localhost:4001/users/octocat/following

Repositories

# Get repo
curl http://localhost:4001/repos/octocat/hello-world

# Create user repo
curl -X POST http://localhost:4001/user/repos \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "new-repo", "private": false}'

# Create org repo
curl -X POST http://localhost:4001/orgs/my-org/repos \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "org-project"}'

# Update repo
curl -X PATCH http://localhost:4001/repos/octocat/hello-world \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated description"}'

# Delete repo (cascades issues, PRs, etc.)
curl -X DELETE http://localhost:4001/repos/octocat/hello-world \
  -H "Authorization: Bearer $TOKEN"

# Topics, languages, contributors, forks, collaborators, tags, transfer

Issues

# List issues (filter by state, labels, assignee, milestone, creator, since)
curl "http://localhost:4001/repos/octocat/hello-world/issues?state=open&labels=bug"

# Create issue
curl -X POST http://localhost:4001/repos/octocat/hello-world/issues \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Bug report", "body": "Details here", "labels": ["bug"]}'

# Get issue
curl http://localhost:4001/repos/octocat/hello-world/issues/1

# Update issue
curl -X PATCH http://localhost:4001/repos/octocat/hello-world/issues/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"state": "closed"}'

# Lock/unlock, timeline, events, assignees

Pull Requests

# List PRs
curl "http://localhost:4001/repos/octocat/hello-world/pulls?state=open"

# Create PR
curl -X POST http://localhost:4001/repos/octocat/hello-world/pulls \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Feature", "head": "feature-branch", "base": "main"}'

# Get PR
curl http://localhost:4001/repos/octocat/hello-world/pulls/1

# Update PR
curl -X PATCH http://localhost:4001/repos/octocat/hello-world/pulls/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated title"}'

# Merge PR (enforces branch protection)
curl -X PUT http://localhost:4001/repos/octocat/hello-world/pulls/1/merge \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"merge_method": "squash"}'

# Commits, files, requested reviewers, update branch

Comments

# Issue comments: full CRUD
curl http://localhost:4001/repos/octocat/hello-world/issues/1/comments
curl -X POST http://localhost:4001/repos/octocat/hello-world/issues/1/comments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body": "Looks good!"}'

# Review comments on PRs
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/comments

# Commit comments
curl http://localhost:4001/repos/octocat/hello-world/commits/abc123/comments

Reviews

# List reviews
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews

# Create review (with inline comments)
curl -X POST http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"event": "APPROVE", "body": "LGTM"}'

# Get, update, submit, dismiss reviews

Labels & Milestones

Full CRUD for labels and milestones. Add/remove labels from issues, replace all labels.

Branches & Git Data

# List branches
curl http://localhost:4001/repos/octocat/hello-world/branches

# Get branch
curl http://localhost:4001/repos/octocat/hello-world/branches/main

# Branch protection CRUD (status checks, PR reviews, enforce admins)
curl -X PUT http://localhost:4001/repos/octocat/hello-world/branches/main/protection \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"required_status_checks": {"strict": true, "contexts": ["ci"]}}'

# Refs, commits, trees (recursive), blobs, tags

Organizations & Teams

# Get org
curl http://localhost:4001/orgs/my-org

# Org members, teams, repos
curl http://localhost:4001/orgs/my-org/members
curl http://localhost:4001/orgs/my-org/teams

Releases

# Create release
curl -X POST http://localhost:4001/repos/octocat/hello-world/releases \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tag_name": "v1.0.0", "name": "v1.0.0"}'

# List, get, latest, by tag, assets, generate notes

Webhooks

# Create webhook (real HTTP delivery on state changes)
curl -X POST http://localhost:4001/repos/octocat/hello-world/hooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"config": {"url": "http://localhost:8080/webhook"}, "events": ["push", "pull_request"]}'

# Full CRUD, ping, test, deliveries
# Org webhooks also supported
# Search repositories
curl "http://localhost:4001/search/repositories?q=language:JavaScript+user:octocat"

# Search issues and PRs
curl "http://localhost:4001/search/issues?q=repo:octocat/hello-world+is:open"

# Search users, code, commits, topics, labels

Actions

# Workflows: list, get, enable/disable, dispatch
# Workflow runs: list, get, cancel, rerun, delete, logs
# Jobs: list, get, logs
# Artifacts: list, get, delete
# Secrets: repo + org CRUD

Checks

# Create check run
curl -X POST http://localhost:4001/repos/octocat/hello-world/check-runs \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI", "head_sha": "abc123", "status": "completed", "conclusion": "success"}'

# Check suites, annotations, rerequest, list by ref
# Automatic suite status rollup from check run results

OAuth

# Authorize (browser flow -- shows user picker)
# GET /login/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...

# Token exchange
curl -X POST http://localhost:4001/login/oauth/access_token \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"client_id": "Iv1.abc123", "client_secret": "secret_abc123", "code": "<code>"}'

# User emails
curl http://localhost:4001/user/emails -H "Authorization: Bearer $TOKEN"

Misc

curl http://localhost:4001/rate_limit
curl http://localhost:4001/meta
curl http://localhost:4001/octocat
curl http://localhost:4001/zen

Common Patterns

Create Repo, Issue, and PR

TOKEN="gho_test_token_admin"
BASE="http://localhost:4001"

# Create repo
curl -X POST $BASE/user/repos \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-project"}'

# Create issue
curl -X POST $BASE/repos/admin/my-project/issues \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "First issue"}'

# Create PR
curl -X POST $BASE/repos/admin/my-project/pulls \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "First PR", "head": "feature", "base": "main"}'

OAuth Flow

  1. Redirect user to $GITHUB_EMULATOR_URL/login/oauth/authorize?client_id=...&redirect_uri=...&scope=user+repo&state=...
  2. User picks a seeded user on the emulator's UI
  3. Emulator redirects back with ?code=...&state=...
  4. Exchange code for token via POST /login/oauth/access_token
  5. Use token to call API endpoints

Upstream Google

<!-- SYNCED from vercel-labs/emulate (skills/google/SKILL.md) --> <!-- Hash: 030d521bf26fe630f0b2efb5447785e1b704ae8eb10eb46cec23a487dbbe9fd0 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

Google OAuth 2.0 / OIDC Emulator

OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE support, ID tokens, and OIDC discovery.

Start

# Google only
npx emulate --service google

# Default port
# http://localhost:4002

Or programmatically:

import { createEmulator } from 'emulate'

const google = await createEmulator({ service: 'google', port: 4002 })
// google.url === 'http://localhost:4002'

Pointing Your App at the Emulator

Environment Variable

GOOGLE_EMULATOR_URL=http://localhost:4002

OAuth URL Mapping

Real Google URLEmulator URL
https://accounts.google.com/o/oauth2/v2/auth$GOOGLE_EMULATOR_URL/o/oauth2/v2/auth
https://oauth2.googleapis.com/token$GOOGLE_EMULATOR_URL/oauth2/token
https://www.googleapis.com/oauth2/v2/userinfo$GOOGLE_EMULATOR_URL/oauth2/v2/userinfo
https://accounts.google.com/.well-known/openid-configuration$GOOGLE_EMULATOR_URL/.well-known/openid-configuration
https://www.googleapis.com/oauth2/v3/certs$GOOGLE_EMULATOR_URL/oauth2/v3/certs

google-auth-library (Node.js)

import { OAuth2Client } from 'google-auth-library'

const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'

const client = new OAuth2Client({
  clientId: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  redirectUri: 'http://localhost:3000/api/auth/callback/google',
})

// Override the endpoints
const authorizeUrl = client.generateAuthUrl({
  access_type: 'offline',
  scope: ['openid', 'email', 'profile'],
})
// Replace the host in authorizeUrl with GOOGLE_URL, or construct manually:
const emulatorAuthorizeUrl = `${GOOGLE_URL}/o/oauth2/v2/auth?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=...&scope=openid+email+profile&response_type=code&state=...`

Auth.js / NextAuth.js

import Google from '@auth/core/providers/google'

Google({
  clientId: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  authorization: {
    url: `${process.env.GOOGLE_EMULATOR_URL}/o/oauth2/v2/auth`,
    params: { scope: 'openid email profile' },
  },
  token: {
    url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/token`,
  },
  userinfo: {
    url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/v2/userinfo`,
  },
})

Passport.js

import { Strategy as GoogleStrategy } from 'passport-google-oauth20'

const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'

new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: 'http://localhost:3000/api/auth/callback/google',
  authorizationURL: `${GOOGLE_URL}/o/oauth2/v2/auth`,
  tokenURL: `${GOOGLE_URL}/oauth2/token`,
  userProfileURL: `${GOOGLE_URL}/oauth2/v2/userinfo`,
}, verifyCallback)

Seed Config

google:
  users:
    - email: testuser@gmail.com
      name: Test User
      given_name: Test
      family_name: User
      picture: https://lh3.googleusercontent.com/a/default-user
      email_verified: true
      locale: en
    - email: dev@example.com
      name: Developer
  oauth_clients:
    - client_id: my-client-id.apps.googleusercontent.com
      client_secret: GOCSPX-secret
      name: My App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/google

When no OAuth clients are configured, the emulator accepts any client_id. With clients configured, strict validation is enforced for client_id, client_secret, and redirect_uri.

API Endpoints

OIDC Discovery

curl http://localhost:4002/.well-known/openid-configuration

Returns the standard OIDC discovery document with all endpoints pointing to the emulator:

{
  "issuer": "http://localhost:4002",
  "authorization_endpoint": "http://localhost:4002/o/oauth2/v2/auth",
  "token_endpoint": "http://localhost:4002/oauth2/token",
  "userinfo_endpoint": "http://localhost:4002/oauth2/v2/userinfo",
  "revocation_endpoint": "http://localhost:4002/oauth2/revoke",
  "jwks_uri": "http://localhost:4002/oauth2/v3/certs",
  "response_types_supported": ["code"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["HS256"],
  "scopes_supported": ["openid", "email", "profile"],
  "code_challenge_methods_supported": ["plain", "S256"]
}

JWKS

curl http://localhost:4002/oauth2/v3/certs

Returns \{ "keys": [] \}. ID tokens are signed with HS256 using an internal secret.

Authorization

# Browser flow -- redirects to a user picker page
curl -v "http://localhost:4002/o/oauth2/v2/auth?\
client_id=my-client-id.apps.googleusercontent.com&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
scope=openid+email+profile&\
response_type=code&\
state=random-state&\
nonce=random-nonce"

Query parameters:

ParamDescription
client_idOAuth client ID
redirect_uriCallback URL
scopeSpace-separated scopes (openid email profile)
stateOpaque state for CSRF protection
nonceNonce for ID token (optional)
code_challengePKCE challenge (optional)
code_challenge_methodplain or S256 (optional)

The emulator renders an HTML page where you select a seeded user. After selection, it redirects to redirect_uri with ?code=...&state=....

Token Exchange

curl -X POST http://localhost:4002/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "code=<authorization_code>&\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
grant_type=authorization_code"

Returns:

{
  "access_token": "google_...",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email profile"
}

The id_token is a JWT (HS256) containing sub, email, email_verified, name, given_name, family_name, picture, locale, and optional nonce.

For PKCE, include code_verifier in the token request.

User Info

curl http://localhost:4002/oauth2/v2/userinfo \
  -H "Authorization: Bearer google_..."

Returns:

{
  "sub": "user-uid",
  "email": "testuser@gmail.com",
  "email_verified": true,
  "name": "Test User",
  "given_name": "Test",
  "family_name": "User",
  "picture": "https://lh3.googleusercontent.com/a/default-user",
  "locale": "en"
}

Token Revocation

curl -X POST http://localhost:4002/oauth2/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=google_..."

Returns 200 OK. The token is removed from the emulator's token map.

Common Patterns

Full Authorization Code Flow

GOOGLE_URL="http://localhost:4002"
CLIENT_ID="my-client-id.apps.googleusercontent.com"
CLIENT_SECRET="GOCSPX-secret"
REDIRECT_URI="http://localhost:3000/api/auth/callback/google"

# 1. Open in browser -- user picks a seeded account
#    $GOOGLE_URL/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc

# 2. After user selection, emulator redirects to:
#    $REDIRECT_URI?code=<code>&state=abc

# 3. Exchange code for tokens
curl -X POST $GOOGLE_URL/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code"

# 4. Fetch user info with the access_token
curl $GOOGLE_URL/oauth2/v2/userinfo \
  -H "Authorization: Bearer <access_token>"

PKCE Flow

# Generate code_verifier and code_challenge
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_')

# 1. Authorize with challenge
# $GOOGLE_URL/o/oauth2/v2/auth?...&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256

# 2. Token exchange with verifier
curl -X POST $GOOGLE_URL/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code&code_verifier=$CODE_VERIFIER"

OIDC Discovery-Based Setup

Libraries that support OIDC discovery (like openid-client) can auto-configure from the discovery document:

import { Issuer } from 'openid-client'

const googleIssuer = await Issuer.discover(
  process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
)

const client = new googleIssuer.Client({
  client_id: process.env.GOOGLE_CLIENT_ID,
  client_secret: process.env.GOOGLE_CLIENT_SECRET,
  redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
})

Upstream Vercel

<!-- SYNCED from vercel-labs/emulate (skills/vercel/SKILL.md) --> <!-- Hash: 768ffadf22343415962e0be8d04606ae43e47944331a8e04bb1e683da0e642c3 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

Vercel API Emulator

Fully stateful Vercel REST API emulation with Vercel-style JSON responses and cursor-based pagination.

Start

# Vercel only
npx emulate --service vercel

# Default port
# http://localhost:4000

Or programmatically:

import { createEmulator } from 'emulate'

const vercel = await createEmulator({ service: 'vercel', port: 4000 })
// vercel.url === 'http://localhost:4000'

Auth

Pass tokens as Authorization: Bearer &lt;token&gt;. All endpoints accept teamId or slug query params for team scoping.

curl http://localhost:4000/v2/user \
  -H "Authorization: Bearer gho_test_token_admin"

When no token is provided, requests fall back to the first seeded user.

Pointing Your App at the Emulator

Environment Variable

VERCEL_EMULATOR_URL=http://localhost:4000

Vercel SDK / Custom Fetch

const VERCEL_API = process.env.VERCEL_EMULATOR_URL ?? 'https://api.vercel.com'

const res = await fetch(`${VERCEL_API}/v10/projects`, {
  headers: { Authorization: `Bearer ${token}` },
})

OAuth URL Mapping

Real Vercel URLEmulator URL
https://vercel.com/integrations/oauth/authorize$VERCEL_EMULATOR_URL/oauth/authorize
https://api.vercel.com/login/oauth/token$VERCEL_EMULATOR_URL/login/oauth/token
https://api.vercel.com/login/oauth/userinfo$VERCEL_EMULATOR_URL/login/oauth/userinfo

Auth.js / NextAuth.js

{
  id: 'vercel',
  name: 'Vercel',
  type: 'oauth',
  authorization: {
    url: `${process.env.VERCEL_EMULATOR_URL}/oauth/authorize`,
  },
  token: {
    url: `${process.env.VERCEL_EMULATOR_URL}/login/oauth/token`,
  },
  userinfo: {
    url: `${process.env.VERCEL_EMULATOR_URL}/login/oauth/userinfo`,
  },
  clientId: process.env.VERCEL_CLIENT_ID,
  clientSecret: process.env.VERCEL_CLIENT_SECRET,
  profile(profile) {
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
      image: profile.picture,
    }
  },
}

Seed Config

tokens:
  gho_test_token_admin:
    login: admin
    scopes: []

vercel:
  users:
    - username: developer
      name: Developer
      email: dev@example.com
  teams:
    - slug: my-team
      name: My Team
  projects:
    - name: my-app
      team: my-team
      framework: nextjs
  integrations:
    - client_id: oac_abc123
      client_secret: secret_abc123
      name: My Vercel App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/vercel

Pagination

Cursor-based pagination using limit, since, and until query params. Responses include a pagination object:

curl "http://localhost:4000/v10/projects?limit=10" \
  -H "Authorization: Bearer $TOKEN"

API Endpoints

User & Teams

# Authenticated user
curl http://localhost:4000/v2/user -H "Authorization: Bearer $TOKEN"

# Update user
curl -X PATCH http://localhost:4000/v2/user \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "New Name"}'

# List teams (cursor paginated)
curl http://localhost:4000/v2/teams -H "Authorization: Bearer $TOKEN"

# Get team (by ID or slug)
curl http://localhost:4000/v2/teams/my-team -H "Authorization: Bearer $TOKEN"

# Create team
curl -X POST http://localhost:4000/v2/teams \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug": "new-team", "name": "New Team"}'

# Update team
curl -X PATCH http://localhost:4000/v2/teams/my-team \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated Team"}'

# List members, add member
curl http://localhost:4000/v2/teams/my-team/members -H "Authorization: Bearer $TOKEN"

Projects

# Create project (with optional env vars and git integration)
curl -X POST http://localhost:4000/v11/projects \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "framework": "nextjs"}'

# List projects (search, cursor pagination)
curl "http://localhost:4000/v10/projects?search=my-app" \
  -H "Authorization: Bearer $TOKEN"

# Get project (includes env vars)
curl http://localhost:4000/v9/projects/my-app \
  -H "Authorization: Bearer $TOKEN"

# Update project
curl -X PATCH http://localhost:4000/v9/projects/my-app \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"framework": "remix"}'

# Delete project (cascades deployments, domains, env vars)
curl -X DELETE http://localhost:4000/v9/projects/my-app \
  -H "Authorization: Bearer $TOKEN"

# Promote aliases status
curl http://localhost:4000/v1/projects/my-app/promote/aliases \
  -H "Authorization: Bearer $TOKEN"

# Protection bypass
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"revoke": false}'

Deployments

# Create deployment (auto-transitions to READY)
curl -X POST http://localhost:4000/v13/deployments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "target": "production"}'

# Get deployment (by ID or URL)
curl http://localhost:4000/v13/deployments/dpl_abc123 \
  -H "Authorization: Bearer $TOKEN"

# List deployments (filter by project, target, state)
curl "http://localhost:4000/v6/deployments?projectId=my-app&target=production" \
  -H "Authorization: Bearer $TOKEN"

# Delete deployment (cascades)
curl -X DELETE http://localhost:4000/v13/deployments/dpl_abc123 \
  -H "Authorization: Bearer $TOKEN"

# Cancel building deployment
curl -X PATCH http://localhost:4000/v12/deployments/dpl_abc123/cancel \
  -H "Authorization: Bearer $TOKEN"

# List deployment aliases
curl http://localhost:4000/v2/deployments/dpl_abc123/aliases \
  -H "Authorization: Bearer $TOKEN"

# Get build events/logs
curl http://localhost:4000/v3/deployments/dpl_abc123/events \
  -H "Authorization: Bearer $TOKEN"

# List deployment files
curl http://localhost:4000/v6/deployments/dpl_abc123/files \
  -H "Authorization: Bearer $TOKEN"

# Upload file (by SHA digest)
curl -X POST http://localhost:4000/v2/files \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/octet-stream" \
  -H "x-vercel-digest: sha256hash" \
  --data-binary @file.txt

Domains

# Add domain (with verification challenge)
curl -X POST http://localhost:4000/v10/projects/my-app/domains \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "example.com"}'

# List domains
curl http://localhost:4000/v9/projects/my-app/domains \
  -H "Authorization: Bearer $TOKEN"

# Get, update, remove domain
curl http://localhost:4000/v9/projects/my-app/domains/example.com \
  -H "Authorization: Bearer $TOKEN"

# Verify domain
curl -X POST http://localhost:4000/v9/projects/my-app/domains/example.com/verify \
  -H "Authorization: Bearer $TOKEN"

Environment Variables

# List env vars (with decrypt option)
curl "http://localhost:4000/v10/projects/my-app/env?decrypt=true" \
  -H "Authorization: Bearer $TOKEN"

# Create env vars (single, batch, or upsert)
curl -X POST http://localhost:4000/v10/projects/my-app/env \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "API_KEY", "value": "secret123", "type": "encrypted", "target": ["production", "preview"]}'

# Get env var
curl http://localhost:4000/v10/projects/my-app/env/env_abc123 \
  -H "Authorization: Bearer $TOKEN"

# Update env var
curl -X PATCH http://localhost:4000/v9/projects/my-app/env/env_abc123 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"value": "newsecret"}'

# Delete env var
curl -X DELETE http://localhost:4000/v9/projects/my-app/env/env_abc123 \
  -H "Authorization: Bearer $TOKEN"

OAuth / Integrations

# Authorize (browser flow -- shows user picker)
# GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...

# Token exchange (supports PKCE)
curl -X POST http://localhost:4000/login/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"client_id": "oac_abc123", "client_secret": "secret_abc123", "code": "<code>", "redirect_uri": "http://localhost:3000/api/auth/callback/vercel"}'

# User info (returns sub, email, name, preferred_username, picture)
curl http://localhost:4000/login/oauth/userinfo \
  -H "Authorization: Bearer $TOKEN"

API Keys

# Manage API keys for programmatic access

Common Patterns

Create Project and Deploy

TOKEN="gho_test_token_admin"
BASE="http://localhost:4000"

# Create project
curl -X POST $BASE/v11/projects \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "framework": "nextjs"}'

# Add env var
curl -X POST $BASE/v10/projects/my-app/env \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "DATABASE_URL", "value": "postgres://...", "type": "encrypted", "target": ["production"]}'

# Create deployment
curl -X POST $BASE/v13/deployments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "target": "production"}'

OAuth Integration Flow

  1. Redirect user to $VERCEL_EMULATOR_URL/oauth/authorize?client_id=...&redirect_uri=...&state=...
  2. User picks a seeded user on the emulator's UI
  3. Emulator redirects back with ?code=...&state=...
  4. Exchange code for token via POST /login/oauth/token
  5. Fetch user info via GET /login/oauth/userinfo

PKCE is supported -- pass code_challenge and code_challenge_method on authorize, then code_verifier on token exchange.

Team-Scoped Requests

All endpoints accept teamId or slug query params:

curl "http://localhost:4000/v10/projects?teamId=team_abc123" \
  -H "Authorization: Bearer $TOKEN"

curl "http://localhost:4000/v10/projects?slug=my-team" \
  -H "Authorization: Bearer $TOKEN"

Upstream

<!-- SYNCED from vercel-labs/emulate (skills/emulate/SKILL.md) --> <!-- Hash: 4cbe1c29594d746595ad412fc40dd5e3be129d48b284ce95680a714bdf73a660 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->

Service Emulation with emulate

Local drop-in replacement services for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation -- not mocks.

Quick Start

npx emulate

All services start with sensible defaults:

ServiceDefault Port
Vercel4000
GitHub4001
Google4002

CLI

# Start all services (zero-config)
emulate

# Start specific services
emulate --service vercel,github

# Custom base port (auto-increments per service)
emulate --port 3000

# Use a seed config file
emulate --seed config.yaml

# Generate a starter config
emulate init

# Generate config for a specific service
emulate init --service vercel

# List available services
emulate list

Options

FlagDefaultDescription
-p, --port4000Base port (auto-increments per service)
-s, --serviceallComma-separated services to enable
--seedauto-detectPath to seed config (YAML or JSON)

The port can also be set via EMULATE_PORT or PORT environment variables.

Programmatic API

npm install emulate

Each call to createEmulator starts a single service:

import { createEmulator } from 'emulate'

const github = await createEmulator({ service: 'github', port: 4001 })
const vercel = await createEmulator({ service: 'vercel', port: 4002 })

github.url   // 'http://localhost:4001'
vercel.url   // 'http://localhost:4002'

await github.close()
await vercel.close()

Options

OptionDefaultDescription
service(required)'github', 'vercel', or 'google'
port4000Port for the HTTP server
seednoneInline seed data (same shape as YAML config)

Instance Methods

MethodDescription
urlBase URL of the running server
reset()Wipe the store and replay seed data
close()Shut down the HTTP server, returns a Promise

Vitest / Jest Setup

import { createEmulator, type Emulator } from 'emulate'

let github: Emulator
let vercel: Emulator

beforeAll(async () => {
  ;[github, vercel] = await Promise.all([
    createEmulator({ service: 'github', port: 4001 }),
    createEmulator({ service: 'vercel', port: 4002 }),
  ])
  process.env.GITHUB_URL = github.url
  process.env.VERCEL_URL = vercel.url
})

afterEach(() => { github.reset(); vercel.reset() })
afterAll(() => Promise.all([github.close(), vercel.close()]))

Configuration

Configuration is optional. The CLI auto-detects config files in this order:

  1. emulate.config.yaml / .yml
  2. emulate.config.json
  3. service-emulator.config.yaml / .yml
  4. service-emulator.config.json

Or pass --seed &lt;file&gt; explicitly. Run emulate init to generate a starter file.

Config Structure

tokens:
  my_token:
    login: admin
    scopes: [repo, user]

vercel:
  users:
    - username: developer
      name: Developer
      email: dev@example.com
  teams:
    - slug: my-team
      name: My Team
  projects:
    - name: my-app
      team: my-team
      framework: nextjs
  integrations:
    - client_id: oac_abc123
      client_secret: secret_abc123
      name: My Vercel App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/vercel

github:
  users:
    - login: octocat
      name: The Octocat
      email: octocat@github.com
  orgs:
    - login: my-org
      name: My Organization
  repos:
    - owner: octocat
      name: hello-world
      language: JavaScript
      auto_init: true
  oauth_apps:
    - client_id: Iv1.abc123
      client_secret: secret_abc123
      name: My Web App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github

google:
  users:
    - email: testuser@example.com
      name: Test User
  oauth_clients:
    - client_id: my-client-id.apps.googleusercontent.com
      client_secret: GOCSPX-secret
      redirect_uris:
        - http://localhost:3000/api/auth/callback/google

Auth

Tokens map to users. Pass them as Authorization: Bearer &lt;token&gt; or Authorization: token &lt;token&gt;. When no tokens are configured, a default gho_test_token_admin is created for the admin user.

Each service also has a fallback user -- if no token is provided, requests authenticate as the first seeded user.

Pointing Your App at the Emulator

Set environment variables to override real service URLs:

GITHUB_EMULATOR_URL=http://localhost:4001
VERCEL_EMULATOR_URL=http://localhost:4000
GOOGLE_EMULATOR_URL=http://localhost:4002

Then use these in your app to construct API and OAuth URLs. See each service's skill for SDK-specific override instructions.

Architecture

packages/
  emulate/           # CLI entry point + programmatic API
  @emulators/
    core/            # HTTP server (Hono), Store, plugin interface, middleware
    vercel/          # Vercel API service plugin
    github/          # GitHub API service plugin
    google/          # Google OAuth 2.0 / OIDC plugin

The core provides a generic Store with typed Collection&lt;T&gt; instances supporting CRUD, indexing, filtering, and pagination. Each service plugin registers routes on the shared Hono app and uses the store for state.

Edit on GitHub

Last updated on

On this page

Emulate Seed ConfigsNew in 2026-04 (emulate 0.4.x)Auto-Discovery (M125 #4)Quick ReferenceQuick StartServices (0.5.0 — 13 emulators)Next.js Adapter (0.4+) — @emulators/adapter-nextSeed Config StructureProgrammatic SDKWebhook DeliveryCI IntegrationParallel Test ExecutionDecision MatrixRelated SkillsCLI ReferenceSDK PatternsRules (5)Auth Tokens: Token Seeding and Credential Hygiene — MEDIUMAuth Token SeedingToken ConfigurationUsing Seeded TokensPermission Testing PatternParallel CI: Per-Worker Port Isolation — HIGHParallel CI Port IsolationPer-Worker Port OffsetJest Worker IsolationCI Matrix IsolationSeed Config: emulate.config.yaml Structure — HIGHSeed Config StructureConfig SectionsService Selection: GitHub, Vercel, and Google — MEDIUMService SelectionService DefaultsMulti-ServiceWebhook Setup: HMAC Delivery and Verification — MEDIUMWebhook SetupWebhook Receiver PatternReferences (7)Api CoverageAPI CoverageGitHub API (:4001)RepositoriesPull RequestsIssuesActions / WorkflowsWebhooksOrganizations & TeamsUsersVercel API (:4000)ProjectsDeploymentsDomainsEnvironment VariablesTeamsGoogle OAuth (:4002)OAuth 2.0Slack Web API (:4003)Chat & ConversationsOAuthApple Authentication (:4004)Microsoft Entra ID (:4005)AWS (:4006)S3SQSIAM & STSStateful BehaviorsCli ReferenceCLI ReferenceCommandsemulate (default)emulate initemulate listFlagsPort DefaultsEnvironment VariablesExit CodesSdk PatternsSDK PatternsBasic UsageMulti-Service SetupTest Framework IntegrationVitestJestEmulator APIcreateEmulate(options)emulator.urlemulator.reset()emulator.close()URL PatternsOctokitVercel SDKfetchState LifecycleUpstream GithubGitHub API EmulatorStartAuthGitHub App JWTPointing Your App at the EmulatorEnvironment VariableOctokitOAuth URL MappingAuth.js / NextAuth.jsSeed ConfigPaginationAPI EndpointsUsersRepositoriesIssuesPull RequestsCommentsReviewsLabels & MilestonesBranches & Git DataOrganizations & TeamsReleasesWebhooksSearchActionsChecksOAuthMiscCommon PatternsCreate Repo, Issue, and PROAuth FlowUpstream GoogleGoogle OAuth 2.0 / OIDC EmulatorStartPointing Your App at the EmulatorEnvironment VariableOAuth URL Mappinggoogle-auth-library (Node.js)Auth.js / NextAuth.jsPassport.jsSeed ConfigAPI EndpointsOIDC DiscoveryJWKSAuthorizationToken ExchangeUser InfoToken RevocationCommon PatternsFull Authorization Code FlowPKCE FlowOIDC Discovery-Based SetupUpstream VercelVercel API EmulatorStartAuthPointing Your App at the EmulatorEnvironment VariableVercel SDK / Custom FetchOAuth URL MappingAuth.js / NextAuth.jsSeed ConfigPaginationAPI EndpointsUser & TeamsProjectsDeploymentsDomainsEnvironment VariablesOAuth / IntegrationsAPI KeysCommon PatternsCreate Project and DeployOAuth Integration FlowTeam-Scoped RequestsUpstreamService Emulation with emulateQuick StartCLIOptionsProgrammatic APIOptionsInstance MethodsVitest / Jest SetupConfigurationConfig StructureAuthPointing Your App at the EmulatorArchitecture