Skip to content

Architecture

How the emulate monorepo is structured — the core plugin system, in-memory store, and shared middleware.

packages/
emulate/ # @pleaseai/emulate — CLI (commander) + programmatic API
kakao/ # @pleaseai/emulate-kakao — kauth OAuth + kapi user/talk
naver/ # @pleaseai/emulate-naver — Naver Login OAuth + profile
toss-payments/ # @pleaseai/emulate-toss-payments — payments + webhooks
firebase/ # @pleaseai/emulate-firebase — Auth, Secure Token, FCM v1
supabase/ # @pleaseai/emulate-supabase — GoTrue + PostgREST
asana/ # @pleaseai/emulate-asana — REST API v1.0
linear/ # @pleaseai/emulate-linear — GraphQL (read-only)
autumn/ # @pleaseai/emulate-autumn — billing
gitlab/ # @pleaseai/emulate-gitlab — GraphQL with introspection
posthog/ # @pleaseai/emulate-posthog — capture + read API + OAuth
spotify/ # @pleaseai/emulate-spotify — token + catalog
workos/ # @pleaseai/emulate-workos — User Management + Vault
x/ # @pleaseai/emulate-x — X API v2
docs/
EMULATOR-CONVENTIONS.md # guide for adding new emulators
website/ # this documentation site (Astro Starlight)

Every service emulator follows the @emulators/* patterns from vercel-labs/emulate and depends on @emulators/core from npm. The runtime and package manager is bun; task orchestration is Turborepo; builds are tsup.

@emulators/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 (Hono-style handlers) and uses the store for state.

Each service is a ServicePlugin that:

  1. Defines its entity types and store collections (entities.ts, store.ts)
  2. Registers HTTP routes with the shared internal app (routes/*.ts)
  3. Provides a seed() for minimal defaults and a seedFromConfig() that consumes its section of emulate.config.yaml
  4. Uses shared middleware for auth, error handling, and webhooks

The CLI’s registry (packages/emulate/src/registry.ts) lazily loads each plugin, so starting one service does not import the other twelve. Multiple services run simultaneously, each on its own port (auto-incremented from the base port).

All state is held in memory with no database. This makes the emulators fast, easy to reset, and ideal for CI pipelines. State is populated from the seed config on startup and mutated by API calls during a test run; reset() restores the seeded snapshot.

Core ships a WebhookDispatcher. Services dispatch events with webhooks.dispatch(event, undefined, payload, "<service>"), and seed configs can register subscriptions declaratively (webhooks: [{ url, events }]).

  • Response JSON field names and formats match the real service exactly (snake_case/camelCase per service).
  • Error responses follow each real service’s error format.
  • Intentional divergences (unsigned JWTs, no Supabase RLS) are documented in Authentication.