@executor-js/emulate
v0.7.1
Published
Local drop-in replacement services for CI and no-network sandboxes
Maintainers
Readme
emulate
This repository is forked from Vercel Labs' emulate project. Useful Software
Co maintains this fork to support deployable emulator surfaces and product
testing flows for our own development and agent-driven use cases.
Local drop-in replacement services for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.
Quick Start
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 - Slack on
http://localhost:4003 - Apple on
http://localhost:4004 - Microsoft on
http://localhost:4005 - Okta on
http://localhost:4006 - AWS on
http://localhost:4007 - Resend on
http://localhost:4008 - Stripe on
http://localhost:4009 - MongoDB Atlas on
http://localhost:4010 - Clerk on
http://localhost:4011 - Spotify on
http://localhost:4012
Every running service also exposes a public control plane under /_emulate:
| Route | Purpose |
|-------|---------|
| GET /_emulate | Human-readable landing page for the service instance |
| GET /_emulate/manifest | Machine-readable service manifest, including supported surfaces, auth capabilities, spec coverage, and resolved connection snippets |
| GET /_emulate/quickstart | Plain-text instructions for humans and agents |
| GET /_emulate/specs | Advertised specs and protocol surfaces |
| GET /_emulate/coverage | Per-operation coverage report with a summary grouped by status (generated, hand-authored, partial, unsupported) |
| GET /_emulate/connections | Copyable SDK, CLI, env, and curl snippets resolved against this instance (optional ?token=, ?client_id=, ?client_secret=) |
| GET /_emulate/openapi | Redirect to the advertised OpenAPI document when one exists |
| GET /_emulate/graphql | Return the GraphQL endpoint when the service exposes one |
| GET /_emulate/mcp | Return the MCP endpoint when the service exposes one |
| GET /_emulate/ledger | Recent API calls with sensitive fields redacted |
| DELETE /_emulate/ledger | Clear the request ledger |
| GET /_emulate/logs | Webhook deliveries plus recent requests |
| GET /_emulate/state | Current emulator store snapshot |
| POST /_emulate/reset | Reset state, webhooks, and request logs, then replay seed data |
| POST /_emulate/seed | Add runtime seed data using the service seed schema |
| POST /_emulate/credentials | Create bearer tokens, API keys, OAuth clients, or client-credentials apps where supported |
| POST /_emulate/instances | Return URLs for a lazily created hosted instance |
The manifest is the machine-readable single source of truth for a service. Each plugin package owns its manifest and serves it at /_emulate/manifest. It describes service identity, supported surfaces, auth capabilities, specs with per-operation coverage, scenarios, seed schema, state model, reset behavior, inspector tabs, request ledger capabilities, copyable connection snippets, and a docs link. OpenAPI, GraphQL, MCP, discovery documents, and OAuth metadata can inform those surfaces, but the emulator only advertises protocols that match the real service shape.
Request ledger
The request ledger is a core feature, not a debug afterthought. Each entry records a correlation id (honored from X-Correlation-Id or X-Request-Id, echoed back in the X-Correlation-Id response header, otherwise generated), the matched route and operation id, method, host, path, query, sanitized request headers and body, the authenticated identity, the response status with a one-line summary, recorded side effects, webhook deliveries, and the request duration. On the hosted Cloudflare surface the ledger is persisted across Durable Object eviction, so it survives instance restarts.
Credential creation follows each service's real shape. For example, GitHub can mint a bearer token for a user, Spotify creates a client credentials app, Google/Microsoft/Apple/Okta/Clerk create OAuth/OIDC clients, Stripe and Resend create API-key style credentials, and AWS advertises provider-specific SDK credentials instead of pretending to be OAuth.
CLI
# Start all services (zero-config)
npx emulate
# Start specific services
npx emulate --service vercel,github
# Custom port
npx emulate --port 3000
# Use a seed config file
npx emulate --seed config.yaml
# Generate a starter config
npx emulate init
# Generate config for a specific service
npx emulate init --service vercel
# List available services
npx emulate listOptions
| 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) |
| --base-url | none | Override advertised base URL (supports {service} template) |
| --portless | off | Serve over HTTPS via portless (auto-registers aliases) |
The port can also be set via EMULATE_PORT or PORT environment variables.
HTTPS with portless
portless gives emulators trusted HTTPS URLs with auto-generated certs and no browser warnings.
# Start the portless proxy (first time only)
portless proxy start
# Start emulate with portless integration
npx emulate start --portlessEach service registers as a portless alias and gets a named HTTPS URL:
github https://github.emulate.localhost
google https://google.emulate.localhost
slack https://slack.emulate.localhostIf portless is not installed, emulate will prompt to install it (npm i -g portless).
The --portless flag overwrites any existing portless aliases matching *.emulate. Aliases are removed automatically when emulate shuts down.
For a custom base URL without portless (any reverse proxy), use --base-url or the EMULATE_BASE_URL env var:
npx emulate start --base-url "https://{service}.myproxy.test"The PORTLESS_URL env var is automatically set by the portless CLI wrapper when running a command through it (e.g. portless github.emulate emulate start), typically to a value like https://{service}.emulate.localhost. It supports {service} interpolation, just like --base-url and EMULATE_BASE_URL. When no explicit baseUrl is provided, it is used as a fallback.
Per-service overrides are also supported in the seed config (these take highest priority over all other base URL sources):
github:
baseUrl: https://github.emulate.localhostDeployed Instances
All services are available on host-based routing when deployed: github, vercel, google, okta, microsoft, spotify, slack, apple, aws, resend, stripe, mongoatlas, clerk, x, workos, and autumn. Each one supports three addressing forms:
https://github.emulators.dev # service host (no instance)
https://github.my-instance.emulators.dev # instance host
https://emulators.dev/github/my-instance # local/path formThe instance host and path form route to the same stateful service instance. The subdomain form is preferred for public examples because the provider base URL is the origin itself, which better matches services such as GitHub that expose API, OAuth, GraphQL, and MCP surfaces under service-owned hosts. The instance control plane is available at https://github.my-instance.emulators.dev/_emulate.
Useful without an instance
The bare service host (for example https://github.emulators.dev) serves a service-level control plane so a human or agent can learn what the service is and connect without first creating an instance. It responds to GET /_emulate, /_emulate/manifest, /_emulate/quickstart, /_emulate/specs, /_emulate/coverage, /_emulate/connections, /_emulate/openapi, and POST /_emulate/instances.
A global catalog lists every hosted service from any host, including the apex:
GET /_emulate/servicesIt returns each service's id, name, description, service host, instance host pattern, path form, and manifest URL, so agents can discover the full surface without repository context.
The apex https://emulators.dev is the emulator catalog: a links-out landing page that lists every emulator and links to each one's service host. The console builds it from GET /_emulate/services. The apex is not the docs site.
Docs
Documentation is a separate site at https://docs.emulators.dev. Per-service docs live at https://docs.emulators.dev/<service>, which is the docsUrl convention each manifest advertises.
Credentials on hosted instances
POST /_emulate/credentials is the canonical, uniform way to mint a credential for any service (a bearer token, API key, or OAuth client depending on the service's auth shape). On the hosted Cloudflare worker the legacy /__seed, /__token, and /__reset endpoints still work, but the /_emulate/* routes are canonical.
Deployment
The emulator worker is named emulate-hosts. It serves emulators.dev/* (the apex catalog) and *.emulators.dev/*, that is every service and instance subdomain. The EmulatorDurableObject class is declared through a wrangler migrations entry (new_classes). Each stateful instance is backed by one EmulatorDurableObject; both the store snapshot and the request ledger are persisted to Durable Object storage so instances survive eviction.
The docs site (apps/web, worker name emulate-docs) serves docs.emulators.dev. Because that host is more specific than *.emulators.dev, it wins for docs traffic.
Programmatic API
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 / 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_EMULATOR_URL = github.url
process.env.VERCEL_EMULATOR_URL = vercel.url
})
afterEach(() => { github.reset(); vercel.reset() })
afterAll(() => Promise.all([github.close(), vercel.close()]))Options
| Option | Default | Description |
|--------|---------|-------------|
| service | (required) | Service name: 'vercel', 'github', 'google', 'slack', 'apple', 'microsoft', 'okta', 'aws', 'resend', 'stripe', 'mongoatlas', 'clerk', 'spotify', 'x', 'workos', or 'autumn' |
| port | 4000 | Port for the HTTP server |
| seed | none | Inline seed data (same shape as YAML config) |
| baseUrl | none | Override advertised base URL. Per-service baseUrl in seed config takes highest priority, then this option, then EMULATE_BASE_URL env var (supports {service}), then PORTLESS_URL (supports {service}, automatically set by the portless CLI wrapper), then http://localhost:<port>. |
Instance methods
| 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
Configuration is optional. The CLI auto-detects config files in this order: emulate.config.yaml / .yml, emulate.config.json, service-emulator.config.yaml / .yml, service-emulator.config.json. Or pass --seed <file> explicitly. Run npx emulate init to generate a starter file.
tokens:
my_token:
login: admin
scopes: [repo, user]
vercel:
users:
- username: developer
name: Developer
email: [email protected]
teams:
- slug: my-team
name: My Team
projects:
- name: my-app
team: my-team
framework: nextjs
github:
users:
- login: octocat
name: The Octocat
email: [email protected]
orgs:
- login: my-org
name: My Organization
repos:
- owner: octocat
name: hello-world
language: JavaScript
auto_init: true
google:
users:
- email: [email protected]
name: Test User
- email: [email protected]
name: Admin
hd: acme.com
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: [email protected]
name: Ops/Review
color_background: "#DDEEFF"
color_text: "#111111"
messages:
- id: msg_welcome
user_email: [email protected]
from: [email protected]
to: [email protected]
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: [email protected]
summary: [email protected]
primary: true
selected: true
time_zone: UTC
calendar_events:
- id: evt_kickoff
user_email: [email protected]
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: [email protected]
name: Docs
mime_type: application/vnd.google-apps.folder
parent_ids: [root]
slack:
team:
name: My Workspace
domain: my-workspace
users:
- name: developer
real_name: Developer
email: [email protected]
profile:
title: Local Developer
status_text: Testing locally
status_emoji: ":computer:"
presence: active
channels:
- name: general
topic: General discussion
- name: random
topic: Random stuff
bots:
- name: my-bot
oauth_apps:
- client_id: "12345.67890"
client_secret: example_client_secret
app_id: A000000001
name: My Slack App
redirect_uris:
- http://localhost:3000/api/auth/callback/slack
scopes:
- chat:write
- channels:read
- channels:history
- channels:join
- channels:manage
- channels:write
- groups:read
- groups:history
- groups:write
- im:read
- im:history
- im:write
- mpim:read
- mpim:history
- mpim:write
- users:read
- users:read.email
- users.profile:read
- users.profile:write
- users:write
- files:read
- files:write
- pins:read
- pins:write
- bookmarks:read
- bookmarks:write
- reactions:read
- reactions:write
- team:read
user_scopes: [users:read, users.profile:read]
bot_name: my-bot
tokens:
- token: xoxb-local-test
user: developer
scopes:
- chat:write
- channels:read
- channels:history
- channels:join
- channels:manage
- channels:write
- groups:read
- groups:history
- groups:write
- im:read
- im:history
- im:write
- mpim:read
- mpim:history
- mpim:write
- users:read
- users:read.email
- users.profile:read
- users.profile:write
- users:write
- files:read
- files:write
- pins:read
- pins:write
- bookmarks:read
- bookmarks:write
- reactions:read
- reactions:write
- team:read
strict_scopes: false
apple:
users:
- email: [email protected]
name: Test User
oauth_clients:
- client_id: com.example.app
team_id: TEAM001
name: My Apple App
redirect_uris:
- http://localhost:3000/api/auth/callback/apple
microsoft:
users:
- email: [email protected]
name: Test User
oauth_clients:
- client_id: example-client-id
client_secret: example-client-secret
name: My Microsoft App
redirect_uris:
- http://localhost:3000/api/auth/callback/microsoft-entra-id
aws:
region: us-east-1
s3:
buckets:
- name: my-app-bucket
- name: my-app-uploads
sqs:
queues:
- name: my-app-events
- name: my-app-dlq
iam:
users:
- user_name: developer
create_access_key: true
roles:
- role_name: lambda-execution-role
description: Role for Lambda function executionOAuth & Integrations
The emulator supports configurable OAuth apps and integrations with strict client validation.
Vercel Integrations
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
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.
GitHub Apps
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.
Slack OAuth Apps
slack:
oauth_apps:
- client_id: "12345.67890"
client_secret: "example_client_secret"
name: "My Slack App"
redirect_uris:
- "http://localhost:3000/api/auth/callback/slack"Apple OAuth Clients
apple:
oauth_clients:
- client_id: "com.example.app"
team_id: "TEAM001"
name: "My Apple App"
redirect_uris:
- "http://localhost:3000/api/auth/callback/apple"Microsoft OAuth Clients
microsoft:
oauth_clients:
- client_id: "example-client-id"
client_secret: "example-client-secret"
name: "My Microsoft App"
redirect_uris:
- "http://localhost:3000/api/auth/callback/microsoft-entra-id"Vercel API
Every endpoint below is fully stateful with Vercel-style JSON responses and cursor-based pagination.
User & Teams
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
Projects
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
Deployments
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)
Domains
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
Environment Variables
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
GitHub API
Every endpoint below is fully stateful. Creates, updates, and deletes persist in memory and affect related entities.
Users
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
Repositories
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
Issues
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
Pull Requests
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
Comments
- 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
Reviews
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 & Milestones
- Labels: full CRUD, add/remove from issues, replace all
- Milestones: full CRUD, state transitions, issue counts
Branches & Git Data
- 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
Organizations & Teams
- Orgs: get, update, list
- Org members: list, check, remove, get/set membership
- Teams: full CRUD, members, repos
Releases
- Releases: full CRUD, latest, by tag
- Release assets: full CRUD, upload
- Generate release notes
Webhooks
- Repo webhooks: full CRUD, ping, test, deliveries
- Org webhooks: full CRUD, ping
- Real HTTP delivery to registered URLs on all state changes
Search
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
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
- 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
Misc
GET /rate_limit- rate limit statusGET /meta- server metadataGET /octocat- ASCII artGET /emojis- emoji URLsGET /zen- random zen phraseGET /versions- API versions
Google OAuth + Gmail, Calendar, and Drive APIs
OAuth 2.0, OpenID Connect, and mutable Google Workspace-style surfaces for local inbox, calendar, and drive flows.
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
Slack API
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, pins, bookmarks, views, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as blocks, attachments, metadata, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Pin and bookmark writes support channel message pins and link bookmarks. View writes support App Home publishing and modal stacks. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records. Supported write state changes dispatch Slack event_callback payloads to configured webhook URLs.
Auth & Chat
POST /api/auth.test- test authenticationPOST /api/chat.postMessage- post message with text or rich payload fields (supports threads viathread_tsand DM user IDs)POST /api/chat.postEphemeral- post ephemeral message outside channel historyPOST /api/chat.update- update message text and rich payload fieldsPOST /api/chat.delete- delete messageGET /api/chat.getPermalink/POST /api/chat.getPermalink- get message permalinkPOST /api/chat.scheduleMessage- schedule pending messagePOST /api/chat.deleteScheduledMessage- delete pending scheduled messagePOST /api/chat.scheduledMessages.list- list pending scheduled messagesPOST /api/chat.meMessage- /me message
Conversations
POST /api/conversations.list- list conversations (cursor pagination,types,exclude_archived)POST /api/conversations.info- get channel infoPOST /api/conversations.create- create channelPOST /api/conversations.archive/conversations.unarchive- archive/restore channelPOST /api/conversations.rename- rename channelPOST /api/conversations.setTopic/conversations.setPurpose- update topic/purposePOST /api/conversations.history- channel history with rich message fieldsPOST /api/conversations.replies- thread replies with rich message fieldsPOST /api/conversations.join/conversations.leave- join/leavePOST /api/conversations.invite/conversations.kick- manage membershipPOST /api/conversations.open/conversations.close- open/close DMs and MPIMsPOST /api/conversations.mark- mark read cursorPOST /api/conversations.members- list members
Users & Reactions
POST /api/users.list- list users (cursor pagination)POST /api/users.info- get user infoPOST /api/users.lookupByEmail- lookup by emailGET /api/users.profile.get/POST /api/users.profile.get- get user profile fieldsPOST /api/users.profile.set- update profile fields, status, and custom fieldsGET /api/users.getPresence/POST /api/users.getPresence- get active or away presencePOST /api/users.setPresence- set the authed user to away or automatic presencePOST /api/reactions.add/reactions.remove/reactions.get- manage reactions
Files
POST /api/files.getUploadURLExternal- create a local external upload sessionPOST /upload/v1/:fileId- receive raw uploaded file bytesPOST /api/files.completeUploadExternal- complete uploads and optionally share file messagesGET /api/files.info/POST /api/files.info- get file metadataGET /api/files.list/POST /api/files.list- list completed filesGET /files-pri/:fileId/:filename- download file bytes with a bearer token that can access the filePOST /api/files.delete- delete a completed file
Pins & Bookmarks
POST /api/pins.add- pin a message to a channelGET /api/pins.list/POST /api/pins.list- list pinned message items for a channelPOST /api/pins.remove- remove a message pin from a channelPOST /api/bookmarks.add- add a link bookmark to a channelPOST /api/bookmarks.edit- update a link bookmarkPOST /api/bookmarks.list- list channel bookmarksPOST /api/bookmarks.remove- remove a bookmark from a channel
Views
POST /api/views.publish- publish or update an App Home view for a userPOST /api/views.open- open a modal viewPOST /api/views.update- update a view byview_idorexternal_idPOST /api/views.push- push a modal view onto the current modal stackPOST /api/views.generateTriggerId- local helper for tests that need a modal trigger id
Modal opens and pushes require values from /api/views.generateTriggerId. Pass the returned value as trigger_id or interactivity_pointer; generate push values with an existing view_id and use them within 3 seconds.
Team, Bots & Webhooks
POST /api/team.info- workspace infoPOST /api/bots.info- bot infoPOST /services/:teamId/:botId/:webhookId- incoming webhook with text or rich payload fields
OAuth
GET /oauth/v2/authorize- authorization (shows user picker)POST /oauth/v2/authorize/callback- local user picker callback that creates the auth codePOST /api/oauth.v2.access- token exchange
Inspector
GET /- tabbed local inspector for conversations, messages, files, views, auth records, incoming webhooks, event subscriptions, and event deliveries
Slack scope checks are relaxed by default so local tests can use simple bearer tokens. Set slack.strict_scopes: true in seed config to make supported Web API methods return Slack-style missing_scope errors with needed and provided fields. Strict mode checks chat:write, channels:read, channels:history, channels:join, channels:manage, channels:write, groups:read, groups:history, groups:write, im:read, im:history, im:write, mpim:read, mpim:history, mpim:write, users:read, users:read.email, users.profile:read, users.profile:write, users:write, files:read, files:write, pins:read, pins:write, bookmarks:read, bookmarks:write, reactions:read, reactions:write, and team:read. Slack lists no method-specific scopes for views.publish, views.open, views.update, or views.push, so the emulator requires auth but does not add strict-scope checks for those methods.
Current Slack limits: Slack Connect, Enterprise Grid admin APIs, Audit Logs API, SCIM, Legal Holds, Socket Mode, slash command and interaction simulation, user groups, reminders, stars, calls, canvases, lists, functions, workflows, chat streaming, legacy files.upload, exact rate limiting, and paid-plan behavior are not implemented.
Apple Sign In
Sign in with Apple emulation with authorization code flow, PKCE support, RS256 ID tokens, and OIDC discovery.
GET /.well-known/openid-configuration- OIDC discovery documentGET /auth/keys- JSON Web Key Set (JWKS)GET /auth/authorize- authorization endpoint (shows user picker)POST /auth/token- token exchange (authorization code and refresh token grants)POST /auth/revoke- token revocation
Microsoft Entra ID
Microsoft Entra ID (Azure AD) v2.0 OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE, client credentials, RS256 ID tokens, and OIDC discovery.
GET /.well-known/openid-configuration- OIDC discovery documentGET /:tenant/v2.0/.well-known/openid-configuration- tenant-scoped OIDC discoveryGET /discovery/v2.0/keys- JSON Web Key Set (JWKS)GET /oauth2/v2.0/authorize- authorization endpoint (shows user picker)POST /oauth2/v2.0/token- token exchange (authorization code, refresh token, client credentials)GET /oidc/userinfo- OpenID Connect user infoGET /v1.0/me- Microsoft Graph user profileGET /oauth2/v2.0/logout- end session / logoutPOST /oauth2/v2.0/revoke- token revocation
AWS
S3, SQS, IAM, and STS emulation with AWS SDK-compatible S3 paths and query-style SQS/IAM/STS endpoints. All responses use AWS-compatible XML.
S3
S3 routes use root paths matching the real AWS S3 wire format, so the official AWS SDK works out of the box with forcePathStyle: true. Legacy /s3/ prefixed paths are also supported for backward compatibility.
GET /- list all bucketsPUT /:bucket- create bucketDELETE /:bucket- delete bucketHEAD /:bucket- check existenceGET /:bucket- list objects (prefix, delimiter, max-keys, continuation-token, start-after)POST /:bucket- presigned POST upload (browser-style multipart form with policy validation)PUT /:bucket/:key- put object (supports copy viax-amz-copy-source)GET /:bucket/:key- get objectHEAD /:bucket/:key- head objectDELETE /:bucket/:key- delete object
SQS
All operations via POST /sqs/ with Action parameter:
CreateQueue,ListQueues,GetQueueUrl,GetQueueAttributesSendMessage,ReceiveMessage,DeleteMessagePurgeQueue,DeleteQueue
IAM
All operations via POST /iam/ with Action parameter:
CreateUser,GetUser,ListUsers,DeleteUserCreateAccessKey,ListAccessKeys,DeleteAccessKeyCreateRole,GetRole,ListRoles,DeleteRole
STS
All operations via POST /sts/ with Action parameter:
GetCallerIdentity,AssumeRole
Next.js Integration
Embed emulators directly in your Next.js app so they run on the same origin. This solves the Vercel preview deployment problem where OAuth callback URLs change with every deployment.
Install
npm install @emulators/adapter-next @emulators/github @emulators/googleOnly install the emulators you need. Each @emulators/* package is published independently.
Route handler
Create a catch-all route that serves emulator traffic:
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
import * as google from '@emulators/google'
export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
services: {
github: {
emulator: github,
seed: {
users: [{ login: 'octocat', name: 'The Octocat' }],
repos: [{ owner: 'octocat', name: 'hello-world', auto_init: true }],
},
},
google: {
emulator: google,
seed: {
users: [{ email: '[email protected]', name: 'Test User' }],
},
},
},
})Auth.js / NextAuth configuration
Point your provider at the emulator paths on the same origin:
import GitHub from 'next-auth/providers/github'
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000'
GitHub({
clientId: 'any-value',
clientSecret: 'any-value',
authorization: { url: `${baseUrl}/emulate/github/login/oauth/authorize` },
token: { url: `${baseUrl}/emulate/github/login/oauth/access_token` },
userinfo: { url: `${baseUrl}/emulate/github/user` },
})No oauth_apps need to be seeded. When none are configured, the emulator skips client_id, client_secret, and redirect_uri validation.
Font files in serverless
Emulator UI pages use bundled fonts. Wrap your Next.js config to include them in the serverless trace:
// next.config.mjs
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
// your normal Next.js config
})If you mount the catch-all at a custom path, pass the matching prefix:
export default withEmulate(nextConfig, { routePrefix: '/api/emulate' })Persistence
By default, emulator state is in-memory and resets on every cold start. To persist state across restarts, pass a persistence adapter:
import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
const kvAdapter = {
async load() { return await kv.get('emulate-state') },
async save(data: string) { await kv.set('emulate-state', data) },
}
export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
services: { github: { emulator: github } },
persistence: kvAdapter,
})For local development, @emulators/core ships filePersistence:
import { filePersistence } from '@emulators/core'
// ...
persistence: filePersistence('.emulate/state.json'),The persistence adapter is called on cold start (load) and after every mutating request (save). Saves are serialized via an internal queue to prevent race conditions.
Architecture
packages/
emulate/ # CLI entry point (commander)
@emulators/
core/ # HTTP server, in-memory store, plugin interface, middleware
adapter-next/ # Next.js App Router integration
vercel/ # Vercel API service
github/ # GitHub API service
google/ # Google OAuth 2.0 / OIDC + Gmail, Calendar, Drive
slack/ # Slack Web API, OAuth v2, incoming webhooks
apple/ # Apple Sign In / OIDC
microsoft/ # Microsoft Entra ID OAuth 2.0 / OIDC + Graph /me
aws/ # AWS S3, SQS, IAM, STS
apps/
web/ # Documentation site (Next.js), deployed to docs.emulators.devThe core provides a generic Store with typed Collection<T> instances supporting CRUD, indexing, filtering, and pagination. Each service plugin registers its routes with the shared internal app and uses the store for state.
Auth
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.
Google: Standard OAuth 2.0 authorization code flow. Configure clients in the seed config.
Slack: All Web API endpoints require Authorization: Bearer <token>. Seeded OAuth apps create local installation records, and OAuth v2 flow with user picker UI creates scoped bot tokens. Optional strict scope mode returns missing_scope when a token lacks a required method scope.
Apple: OIDC authorization code flow with RS256 ID tokens. On first auth per user/client pair, a user JSON blob is included.
Microsoft: OIDC authorization code flow with PKCE support. Also supports client credentials grants. Microsoft Graph /v1.0/me available.
AWS: Bearer tokens or IAM access key credentials. Default key pair always seeded: AKIAIOSFODNN7EXAMPLE / wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY.
