Trending-skills vercel-labs-emulate

Local drop-in API emulation for Vercel, GitHub, and Google services in CI and no-network sandboxes

install
source · Clone the upstream repo
git clone https://github.com/Aradotso/trending-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/vercel-labs-emulate" ~/.claude/skills/aradotso-trending-skills-vercel-labs-emulate && rm -rf "$T"
manifest: skills/vercel-labs-emulate/SKILL.md
source content

vercel-labs/emulate

Skill by ara.so — Daily 2026 Skills collection.

emulate
provides fully stateful, production-fidelity local HTTP servers that replace Vercel, GitHub, and Google APIs. Designed for CI pipelines and no-network sandboxes — not mocks, real in-memory state with proper pagination, OAuth, webhooks, and cascading deletes.

Installation

# CLI (no install needed)
npx emulate

# Or install as a dev dependency
npm install --save-dev emulate

CLI Usage

# Start all services with defaults
npx emulate

# Start specific services
npx emulate --service vercel,github

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

# Start with seed data
npx emulate --seed emulate.config.yaml

# Generate a starter config
npx emulate init

# Generate config for a specific service
npx emulate init --service github

# List available services
npx emulate list

Default ports:

  • Vercel
    http://localhost:4000
  • GitHub
    http://localhost:4001
  • Google
    http://localhost:4002

Port can also be set via

EMULATE_PORT
or
PORT
environment variables.

Programmatic API

import { createEmulator, type Emulator } from 'emulate'

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

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

// Reset state (replays seed data)
github.reset()

// Shutdown
await github.close()
await vercel.close()

Options

OptionDefaultDescription
service
(required)
'github'
,
'vercel'
, or
'google'
port
4000
Port for the HTTP server
seed
noneInline seed data object (same shape as YAML config)

Instance Methods

MethodDescription
url
Base URL of the running server
reset()
Wipe in-memory store and replay seed data
close()
Shut down the server (returns Promise)

Vitest / Jest Setup

// vitest.setup.ts
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()]))
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['./vitest.setup.ts'],
    environment: 'node',
  },
})

Seed Configuration

Create

emulate.config.yaml
in your project root (auto-detected):

# Auth tokens
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

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

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

Inline Seed (Programmatic)

const github = await createEmulator({
  service: 'github',
  port: 4001,
  seed: {
    users: [
      { login: 'testuser', name: 'Test User', email: 'test@example.com' }
    ],
    repos: [
      { owner: 'testuser', name: 'my-repo', language: 'TypeScript', auto_init: true }
    ],
  },
})

OAuth Configuration

GitHub OAuth Apps

github:
  oauth_apps:
    - client_id: $GITHUB_CLIENT_ID
      client_secret: $GITHUB_CLIENT_SECRET
      name: My Web App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github

Without

oauth_apps
configured, the emulator accepts any
client_id
(backward-compatible). With apps configured, strict validation is enforced.

GitHub Apps (JWT Auth)

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

Sign JWTs with

{ iss: "<app_id>" }
using RS256 — the emulator verifies the signature.

Vercel Integrations

vercel:
  integrations:
    - client_id: $VERCEL_CLIENT_ID
      client_secret: $VERCEL_CLIENT_SECRET
      name: My Vercel App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/vercel

Real-World Test Patterns

Testing a GitHub API Client

import { createEmulator } from 'emulate'
import { Octokit } from '@octokit/rest'

describe('GitHub integration', () => {
  let emulator: Awaited<ReturnType<typeof createEmulator>>
  let octokit: Octokit

  beforeAll(async () => {
    emulator = await createEmulator({
      service: 'github',
      port: 4001,
      seed: {
        users: [{ login: 'testuser', name: 'Test User' }],
        repos: [{ owner: 'testuser', name: 'my-repo', auto_init: true }],
      },
    })

    octokit = new Octokit({
      baseUrl: emulator.url,
      auth: 'any-token',
    })
  })

  afterEach(() => emulator.reset())
  afterAll(() => emulator.close())

  it('creates and fetches an issue', async () => {
    const { data: issue } = await octokit.issues.create({
      owner: 'testuser',
      repo: 'my-repo',
      title: 'Test issue',
      body: 'This is a test',
    })

    expect(issue.number).toBe(1)
    expect(issue.state).toBe('open')

    const { data: fetched } = await octokit.issues.get({
      owner: 'testuser',
      repo: 'my-repo',
      issue_number: issue.number,
    })

    expect(fetched.title).toBe('Test issue')
  })
})

Testing a Vercel Deployment Workflow

import { createEmulator } from 'emulate'

describe('Vercel deployment', () => {
  let emulator: Awaited<ReturnType<typeof createEmulator>>

  beforeAll(async () => {
    emulator = await createEmulator({
      service: 'vercel',
      port: 4002,
      seed: {
        users: [{ username: 'dev', email: 'dev@example.com' }],
        projects: [{ name: 'my-app', framework: 'nextjs' }],
      },
    })
    process.env.VERCEL_API_URL = emulator.url
  })

  afterEach(() => emulator.reset())
  afterAll(() => emulator.close())

  it('creates a deployment and transitions to READY', async () => {
    const res = await fetch(`${emulator.url}/v13/deployments`, {
      method: 'POST',
      headers: {
        Authorization: 'Bearer any-token',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: 'my-app', target: 'production' }),
    })

    const deployment = await res.json()
    expect(deployment.readyState).toBe('READY')
  })
})

Testing Multiple Services Together

import { createEmulator, type Emulator } from 'emulate'

let github: Emulator
let vercel: Emulator
let google: Emulator

beforeAll(async () => {
  ;[github, vercel, google] = await Promise.all([
    createEmulator({ service: 'github', port: 4001 }),
    createEmulator({ service: 'vercel', port: 4002 }),
    createEmulator({ service: 'google', port: 4003 }),
  ])

  // Point your app's env vars at local emulators
  process.env.GITHUB_API_URL = github.url
  process.env.VERCEL_API_URL = vercel.url
  process.env.GOOGLE_API_URL = google.url
})

afterEach(() => {
  github.reset()
  vercel.reset()
  google.reset()
})

afterAll(() => Promise.all([github.close(), vercel.close(), google.close()]))

CI Configuration

GitHub Actions

# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Run tests with emulated APIs
        run: npm test
        env:
          # Emulators start in vitest.setup.ts — no extra service needed
          NODE_ENV: test

Docker / No-Network Sandbox

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Tests start emulators programmatically — no outbound network needed
RUN npm test

Key API Endpoints Reference

GitHub Emulator

GET    /user                                    # authenticated user
GET    /repos/:owner/:repo                      # get repo
POST   /user/repos                              # create repo
POST   /repos/:owner/:repo/issues               # create issue
PATCH  /repos/:owner/:repo/issues/:number       # update issue
POST   /repos/:owner/:repo/pulls                # create PR
PUT    /repos/:owner/:repo/pulls/:number/merge  # merge PR
GET    /search/repositories                     # search repos
GET    /search/issues                           # search issues

Vercel Emulator

GET    /v2/user                          # authenticated user
GET    /v2/teams                         # list teams
POST   /v11/projects                     # create project
GET    /v10/projects                     # list projects
POST   /v13/deployments                  # create deployment (auto → READY)
GET    /v13/deployments/:idOrUrl         # get deployment
POST   /v10/projects/:id/env             # create env vars
GET    /v10/projects/:id/env             # list env vars

Troubleshooting

Port already in use

# Use a different base port
npx emulate --port 5000
# Or set via env
EMULATE_PORT=5000 npx emulate

Tests interfering with each other

// Always call reset() in afterEach, not afterAll
afterEach(() => emulator.reset())

OAuth strict validation rejecting requests

  • If you configure
    oauth_apps
    or
    integrations
    , only matching
    client_id
    values are accepted
  • Remove the
    oauth_apps
    block to fall back to accept-any mode

Emulator not receiving requests from app code

// Make sure your app reads the URL from env at request time, not module load time
// ✅ Good
async function fetchUser() {
  return fetch(`${process.env.GITHUB_API_URL}/user`)
}

// ❌ Bad — captured before emulator starts
const API_URL = process.env.GITHUB_API_URL

GitHub App JWT auth failing

  • JWT must have
    { iss: "<app_id>" }
    as a string or number matching the configured
    app_id
  • Must be signed RS256 with the exact private key from config
  • The emulator verifies the signature — use a real RSA key pair in tests