Local drop-in replacement services for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.
npx emulateAll services start with sensible defaults. No config file needed:
- Vercel on
http://localhost:4000 - GitHub on
http://localhost:4001 - Google on
http://localhost:4002
# Start all services (zero-config)
emulate
# Start specific services
emulate --service vercel,github
# Custom port
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| Flag | Default | Description |
|---|---|---|
-p, --port |
4000 |
Base port (auto-increments per service) |
-s, --service |
all | Comma-separated services to enable |
--seed |
auto-detect | Path to seed config (YAML or JSON) |
The port can also be set via EMULATE_PORT or PORT environment variables.
npm install emulateEach 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()// 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()]))| Option | Default | Description |
|---|---|---|
service |
(required) | Service to emulate: 'github', 'vercel', or 'google' |
port |
4000 |
Port for the HTTP server |
seed |
none | Inline seed data (same shape as YAML config) |
| Method | Description |
|---|---|
url |
Base URL of the running server |
reset() |
Wipe the store and replay seed data |
close() |
Shut down the HTTP server, returns a Promise |
Configuration is optional. To customize seed data, create emulate.config.yaml in your project root (or pass --seed):
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: GOCSPX-secret
redirect_uris:
- http://localhost:3000/api/auth/callback/google
labels:
- id: Label_ops
user_email: testuser@example.com
name: Ops/Review
color_background: "#DDEEFF"
color_text: "#111111"
messages:
- id: msg_welcome
user_email: testuser@example.com
from: welcome@example.com
to: testuser@example.com
subject: Welcome to the Gmail emulator
body_text: You can now test Gmail, Calendar, and Drive flows locally.
label_ids: [INBOX, UNREAD, CATEGORY_UPDATES]
calendars:
- id: primary
user_email: testuser@example.com
summary: testuser@example.com
primary: true
selected: true
time_zone: UTC
calendar_events:
- id: evt_kickoff
user_email: testuser@example.com
calendar_id: primary
summary: Project Kickoff
start_date_time: 2025-01-10T09:00:00.000Z
end_date_time: 2025-01-10T09:30:00.000Z
drive_items:
- id: drv_docs
user_email: testuser@example.com
name: Docs
mime_type: application/vnd.google-apps.folder
parent_ids: [root]The emulator supports configurable OAuth apps and integrations with strict client validation.
vercel:
integrations:
- client_id: "oac_abc123"
client_secret: "secret_abc123"
name: "My Vercel App"
redirect_uris:
- "http://localhost:3000/api/auth/callback/vercel"github:
oauth_apps:
- client_id: "Iv1.abc123"
client_secret: "secret_abc123"
name: "My Web App"
redirect_uris:
- "http://localhost:3000/api/auth/callback/github"If no oauth_apps are configured, the emulator accepts any client_id (backward-compatible). With apps configured, strict validation is enforced.
Full GitHub App support with JWT authentication and installation access tokens:
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]
webhook_url: "http://localhost:3000/webhooks/github"
webhook_secret: "my-secret"
installations:
- installation_id: 100
account: my-org
repository_selection: allJWT authentication: sign a JWT with { iss: "<app_id>" } using the app's private key (RS256). The emulator verifies the signature and resolves the app.
App webhook delivery: When events occur on repos where a GitHub App is installed, the emulator mirrors real GitHub behavior:
- All webhook payloads (including repo and org hooks) include an
installationfield with{ id, node_id }. - If the app has a
webhook_url, the emulator delivers the event there with theinstallationfield and (if configured) anX-Hub-Signature-256header signed withwebhook_secret.
Every endpoint below is fully stateful with Vercel-style JSON responses and cursor-based pagination.
GET /v2/user- authenticated userPATCH /v2/user- update userGET /v2/teams- list teams (cursor paginated)GET /v2/teams/:teamId- get team (by ID or slug)POST /v2/teams- create teamPATCH /v2/teams/:teamId- update teamGET /v2/teams/:teamId/members- list membersPOST /v2/teams/:teamId/members- add member
POST /v11/projects- create project (with optional env vars and git integration)GET /v10/projects- list projects (search, cursor pagination)GET /v9/projects/:idOrName- get project (includes env vars)PATCH /v9/projects/:idOrName- update projectDELETE /v9/projects/:idOrName- delete project (cascades)GET /v1/projects/:projectId/promote/aliases- promote aliases statusPATCH /v1/projects/:idOrName/protection-bypass- manage bypass secrets
POST /v13/deployments- create deployment (auto-transitions to READY)GET /v13/deployments/:idOrUrl- get deployment (by ID or URL)GET /v6/deployments- list deployments (filter by project, target, state)DELETE /v13/deployments/:id- delete deployment (cascades)PATCH /v12/deployments/:id/cancel- cancel building deploymentGET /v2/deployments/:id/aliases- list deployment aliasesGET /v3/deployments/:idOrUrl/events- get build events/logsGET /v6/deployments/:id/files- list deployment filesPOST /v2/files- upload file (by SHA digest)
POST /v10/projects/:idOrName/domains- add domain (with verification challenge)GET /v9/projects/:idOrName/domains- list domainsGET /v9/projects/:idOrName/domains/:domain- get domainPATCH /v9/projects/:idOrName/domains/:domain- update domainDELETE /v9/projects/:idOrName/domains/:domain- remove domainPOST /v9/projects/:idOrName/domains/:domain/verify- verify domain
GET /v10/projects/:idOrName/env- list env vars (with decrypt option)POST /v10/projects/:idOrName/env- create env vars (single, batch, upsert)GET /v10/projects/:idOrName/env/:id- get env varPATCH /v9/projects/:idOrName/env/:id- update env varDELETE /v9/projects/:idOrName/env/:id- delete env var
Every endpoint below is fully stateful. Creates, updates, and deletes persist in memory and affect related entities.
GET /user- authenticated userPATCH /user- update profileGET /users/:username- get userGET /users- list usersGET /users/:username/repos- list user reposGET /users/:username/orgs- list user orgsGET /users/:username/followers- list followersGET /users/:username/following- list following
GET /repos/:owner/:repo- get repoPOST /user/repos- create user repoPOST /orgs/:org/repos- create org repoPATCH /repos/:owner/:repo- update repoDELETE /repos/:owner/:repo- delete repo (cascades)GET/PUT /repos/:owner/:repo/topics- get/replace topicsGET /repos/:owner/:repo/languages- languagesGET /repos/:owner/:repo/contributors- contributorsGET /repos/:owner/:repo/forks- list forksPOST /repos/:owner/:repo/forks- create forkGET/PUT/DELETE /repos/:owner/:repo/collaborators/:username- collaboratorsGET /repos/:owner/:repo/collaborators/:username/permissionPOST /repos/:owner/:repo/transfer- transfer repoGET /repos/:owner/:repo/tags- list tags
GET /repos/:owner/:repo/issues- list (filter by state, labels, assignee, milestone, creator, since)POST /repos/:owner/:repo/issues- createGET /repos/:owner/:repo/issues/:number- getPATCH /repos/:owner/:repo/issues/:number- update (state transitions, events)PUT/DELETE /repos/:owner/:repo/issues/:number/lock- lock/unlockGET /repos/:owner/:repo/issues/:number/timeline- timeline eventsGET /repos/:owner/:repo/issues/:number/events- eventsPOST/DELETE /repos/:owner/:repo/issues/:number/assignees- manage assignees
GET /repos/:owner/:repo/pulls- list (filter by state, head, base)POST /repos/:owner/:repo/pulls- createGET /repos/:owner/:repo/pulls/:number- getPATCH /repos/:owner/:repo/pulls/:number- updatePUT /repos/:owner/:repo/pulls/:number/merge- merge (with branch protection enforcement)GET /repos/:owner/:repo/pulls/:number/commits- list commitsGET /repos/:owner/:repo/pulls/:number/files- list filesPOST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers- manage reviewersPUT /repos/:owner/:repo/pulls/:number/update-branch- update branch
- Issue comments: full CRUD on
/repos/:owner/:repo/issues/:number/comments - Review comments: full CRUD on
/repos/:owner/:repo/pulls/:number/comments - Commit comments: full CRUD on
/repos/:owner/:repo/commits/:sha/comments - Repo-wide listings for each type
GET /repos/:owner/:repo/pulls/:number/reviews- listPOST /repos/:owner/:repo/pulls/:number/reviews- create (with inline comments)GET/PUT /repos/:owner/:repo/pulls/:number/reviews/:id- get/updatePOST /repos/:owner/:repo/pulls/:number/reviews/:id/events- submitPUT /repos/:owner/:repo/pulls/:number/reviews/:id/dismissals- dismiss
- Labels: full CRUD, add/remove from issues, replace all
- Milestones: full CRUD, state transitions, issue counts
- Branches: list, get, protection CRUD (status checks, PR reviews, enforce admins)
- Refs: get, match, create, update, delete
- Commits: get, create
- Trees: get (with recursive), create (with inline content)
- Blobs: get, create
- Tags: get, create
- Orgs: get, update, list
- Org members: list, check, remove, get/set membership
- Teams: full CRUD, members, repos
- Releases: full CRUD, latest, by tag
- Release assets: full CRUD, upload
- Generate release notes
- Repo webhooks: full CRUD, ping, test, deliveries
- Org webhooks: full CRUD, ping
- Real HTTP delivery to registered URLs on all state changes
GET /search/repositories- full query syntax (user, org, language, topic, stars, forks, etc.)GET /search/issues- issues + PRs (repo, is, author, label, milestone, state, etc.)GET /search/users- users + orgsGET /search/code- blob content searchGET /search/commits- commit message searchGET /search/topics- topic searchGET /search/labels- label search
- 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
- Check runs: create, update, get, annotations, rerequest, list by ref/suite
- Check suites: create, get, preferences, rerequest, list by ref
- Automatic suite status rollup from check run results
GET /rate_limit- rate limit statusGET /meta- server metadataGET /octocat- ASCII artGET /emojis- emoji URLsGET /zen- random zen phraseGET /versions- API versions
OAuth 2.0, OpenID Connect, and mutable Google Workspace-style surfaces for local inbox, calendar, and drive flows.
This stays under a single google: service because the Gmail API is used by both consumer Google accounts and Google Workspace accounts. A separate Workspace-specific service would only make sense once we add admin or tenant-level APIs that do not belong in the basic Google/Gmail emulator.
GET /o/oauth2/v2/auth- authorization endpointPOST /oauth2/token- token exchangeGET /oauth2/v2/userinfo- get user infoGET /.well-known/openid-configuration- OIDC discovery documentGET /oauth2/v3/certs- JSON Web Key Set (JWKS)GET /gmail/v1/users/:userId/messages- list messages withq,labelIds,maxResults, andpageTokenGET /gmail/v1/users/:userId/messages/:id- fetch a Gmail-style message payload infull,metadata,minimal, orrawformatsGET /gmail/v1/users/:userId/messages/:messageId/attachments/:id- fetch attachment bodiesPOST /gmail/v1/users/:userId/messages/send- create sent mail fromrawMIME or structured fieldsPOST /gmail/v1/users/:userId/messages/import- import inbox mailPOST /gmail/v1/users/:userId/messages- insert a message directlyPOST /gmail/v1/users/:userId/messages/:id/modify- add/remove labels on one messagePOST /gmail/v1/users/:userId/messages/batchModify- add/remove labels across many messagesPOST /gmail/v1/users/:userId/messages/:id/trashandPOST /gmail/v1/users/:userId/messages/:id/untrashGET /gmail/v1/users/:userId/drafts,POST /gmail/v1/users/:userId/drafts,GET /gmail/v1/users/:userId/drafts/:id,PUT /gmail/v1/users/:userId/drafts/:id,POST /gmail/v1/users/:userId/drafts/:id/send,DELETE /gmail/v1/users/:userId/drafts/:idPOST /gmail/v1/users/:userId/threads/:id/modify- add/remove labels across a threadGET /gmail/v1/users/:userId/threadsandGET /gmail/v1/users/:userId/threads/:idGET /gmail/v1/users/:userId/labels,POST /gmail/v1/users/:userId/labels,PATCH /gmail/v1/users/:userId/labels/:id,DELETE /gmail/v1/users/:userId/labels/:idGET /gmail/v1/users/:userId/history,POST /gmail/v1/users/:userId/watch,POST /gmail/v1/users/:userId/stopGET /gmail/v1/users/:userId/settings/filters,POST /gmail/v1/users/:userId/settings/filters,DELETE /gmail/v1/users/:userId/settings/filters/:idGET /gmail/v1/users/:userId/settings/forwardingAddresses,GET /gmail/v1/users/:userId/settings/sendAsGET /calendar/v3/users/:userId/calendarList,GET /calendar/v3/calendars/:calendarId/events,POST /calendar/v3/calendars/:calendarId/events,DELETE /calendar/v3/calendars/:calendarId/events/:eventId,POST /calendar/v3/freeBusyGET /drive/v3/files,GET /drive/v3/files/:fileId,POST /drive/v3/files,PATCH /drive/v3/files/:fileId,PUT /drive/v3/files/:fileId,POST /upload/drive/v3/files
The Google plugin still does not cover every Google API edge case, but Gmail, Calendar, and Drive now have enough mutable surface to support realistic local automation flows without stuffing everything into static seed config.
packages/
emulate/ # CLI entry point (commander)
@emulators/
core/ # HTTP server, in-memory store, plugin interface, middleware
vercel/ # Vercel API service
github/ # GitHub API service
google/ # Google OAuth 2.0 / OIDC + Gmail, Calendar, and Drive APIs
apps/
web/ # Documentation site (Next.js)
The core provides a generic Store with typed Collection<T> instances supporting CRUD, indexing, filtering, and pagination. Each service plugin registers its routes on the shared Hono app and uses the store for state.
Tokens are configured in the seed config and map to users. Pass them as Authorization: Bearer <token> or Authorization: token <token>.
Vercel: All endpoints accept teamId or slug query params for team scoping. Pagination uses cursor-based limit/since/until with pagination response objects.
GitHub: Public repo endpoints work without auth. Private repos and write operations require a valid token. Pagination uses page/per_page with Link headers.