# The Registry — How Each Service Gets Wired In

> How registry.ts acts as the master switchboard: each ServiceEntry knows how to lazy-load its plugin package, what endpoints it covers, what default auth fallback to use, and what starter config to generate with `emulate init`.

- Repository: vercel-labs/emulate
- GitHub: https://github.com/vercel-labs/emulate
- Human wiki: https://grok-wiki.com/public/wiki/vercel-labs-emulate-ddc6091d171d
- Complete Markdown: https://grok-wiki.com/public/wiki/vercel-labs-emulate-ddc6091d171d/llms-full.txt

## Source Files

- `packages/emulate/src/registry.ts`
- `packages/emulate/src/commands/list.ts`
- `packages/emulate/src/base-url.ts`
- `packages/emulate/src/portless.ts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/emulate/src/registry.ts](packages/emulate/src/registry.ts)
- [packages/emulate/src/commands/start.ts](packages/emulate/src/commands/start.ts)
- [packages/emulate/src/commands/init.ts](packages/emulate/src/commands/init.ts)
- [packages/emulate/src/commands/list.ts](packages/emulate/src/commands/list.ts)
- [packages/emulate/src/base-url.ts](packages/emulate/src/base-url.ts)
- [packages/emulate/src/portless.ts](packages/emulate/src/portless.ts)
- [packages/emulate/src/index.ts](packages/emulate/src/index.ts)
</details>

# The Registry — How Each Service Gets Wired In

`registry.ts` is the single file that tells the `emulate` CLI what services exist and everything it needs to know about each one. It is the master switchboard: before a single HTTP server starts, before a config file is written, and before the `list` command prints its table, the CLI looks here first.

Every service — Vercel, GitHub, Google, Slack, Apple, Microsoft, Okta, AWS, Resend, Stripe, MongoDB Atlas, and Clerk — has one entry in `SERVICE_REGISTRY`. That entry answers four questions at once: what is this service called, which API endpoints does it cover, how should it load its plugin code, what identity should an unauthenticated request fall back to, and what does a good starter config look like? Keeping those four concerns together in one record is what lets the rest of the CLI stay small and data-driven.

## The `ServiceEntry` Shape

Each entry in `SERVICE_REGISTRY` conforms to the `ServiceEntry` interface:

```typescript
// packages/emulate/src/registry.ts:9-15
export interface ServiceEntry {
  label: string;
  endpoints: string;
  load(): Promise<LoadedService>;
  defaultFallback(svcSeedConfig?: Record<string, unknown>): AuthFallback;
  initConfig: Record<string, unknown>;
}
```

| Field | Type | Purpose |
|---|---|---|
| `label` | `string` | Human-readable one-liner shown by `emulate list` |
| `endpoints` | `string` | Comma-separated summary of supported API areas |
| `load()` | `() => Promise<LoadedService>` | Lazy-loads the plugin package on demand |
| `defaultFallback()` | `(cfg?) => AuthFallback` | Provides a fallback identity when no token matches |
| `initConfig` | `Record<string, unknown>` | Template written to `emulate.config.yaml` by `emulate init` |

`LoadedService`, returned by `load()`, carries up to three things: the `plugin` object the HTTP server uses, an optional `seedFromConfig` function that pre-populates the in-memory store from the YAML config, and an optional `createAppKeyResolver` for services that issue signed tokens (currently only GitHub Apps).

```typescript
// packages/emulate/src/registry.ts:3-7
export interface LoadedService {
  plugin: ServicePlugin;
  seedFromConfig?(store: Store, baseUrl: string, config: unknown, webhooks?: WebhookDispatcher): void;
  createAppKeyResolver?(store: Store): AppKeyResolver;
}
```

## How Lazy Loading Works

The `load()` function for each entry does a dynamic `import()` of its scoped package. No service plugin is loaded at process start — it is loaded when the `startCommand` actually needs it.

```typescript
// packages/emulate/src/registry.ts:38-41 (Vercel entry)
async load() {
  const mod = await import("@emulators/vercel");
  return { plugin: mod.vercelPlugin, seedFromConfig: mod.seedFromConfig };
},
```

The GitHub entry additionally wires up an `AppKeyResolver` inline so the core server can verify GitHub App JWT tokens against data stored in the emulated GitHub store:

```typescript
// packages/emulate/src/registry.ts:67-84
async load() {
  const mod = await import("@emulators/github");
  return {
    plugin: mod.githubPlugin,
    seedFromConfig: mod.seedFromConfig,
    createAppKeyResolver(store: Store): AppKeyResolver {
      return (appId: number) => {
        try {
          const gh = mod.getGitHubStore(store);
          const ghApp = gh.apps.all().find((a) => a.app_id === appId);
          if (!ghApp) return null;
          return { privateKey: ghApp.private_key, slug: ghApp.slug, name: ghApp.name };
        } catch { return null; }
      };
    },
  };
},
```

This is the only service that needs a key resolver because GitHub App requests carry JWTs signed with a private key, not plain bearer tokens.

## The `defaultFallback` Field

When an incoming HTTP request carries no token, or carries a token that is not in the seed config, the `emulate` server needs to act as *someone*. The `defaultFallback` function provides that identity.

Most services derive the fallback from the first user listed in the seed config, and fall back to a hard-coded safe default if no config is present. The pattern varies slightly per service identity model:

| Service | Identity field | Hard-coded default |
|---|---|---|
| `vercel` | `cfg.users[0].username` | `"admin"` |
| `github` | `cfg.users[0].login` | `"admin"` with full OAuth scopes |
| `google`, `microsoft`, `apple`, `clerk` | `cfg.users[0].email` | Service-specific email |
| `okta` | `cfg.users[0].login` or `cfg.users[0].email` | `"testuser@okta.local"` |
| `slack` | _(always)_ | Slack user ID `"U000000001"` |
| `aws`, `resend`, `stripe`, `mongoatlas` | _(always)_ | Service-specific admin identity |

```typescript
// packages/emulate/src/registry.ts:86-89 (GitHub)
defaultFallback(cfg) {
  const firstLogin = (cfg?.users as Array<{ login?: string }> | undefined)?.[0]?.login ?? "admin";
  return { login: firstLogin, id: 1, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
},
```

The fallback is passed as `fallbackUser` to `createServer` in `start.ts`. Sources: [packages/emulate/src/commands/start.ts:172-179]().

## The `initConfig` Field and `emulate init`

`initConfig` is the template that `emulate init` serializes to `emulate.config.yaml`. It contains realistic-but-obviously-fake sample data: placeholder client IDs, redirect URIs pointing at `localhost:3000`, and seed users that match what a Next.js or Node app would need during development.

`initCommand` simply reads this field and calls `yamlStringify`:

```typescript
// packages/emulate/src/commands/init.ts:26-31
const entry = SERVICE_REGISTRY[options.service as ServiceName];
// ...
config = { ...DEFAULT_TOKENS, ...entry.initConfig };
const content = yamlStringify(config);
writeFileSync(fullPath, content, "utf-8");
```

When `--service all` is passed, every `initConfig` in the registry is merged together into one YAML file:

```typescript
// packages/emulate/src/commands/init.ts:21-24
config = { ...DEFAULT_TOKENS };
for (const name of SERVICE_NAMES) {
  Object.assign(config, SERVICE_REGISTRY[name].initConfig);
}
```

Sources: [packages/emulate/src/commands/init.ts:10-39]().

## Service Name Enumeration

The registry also exports two name-related values consumed throughout the CLI:

```typescript
// packages/emulate/src/registry.ts:17-32
const SERVICE_NAME_LIST = [
  "vercel", "github", "google", "slack", "apple",
  "microsoft", "okta", "aws", "resend", "stripe",
  "mongoatlas", "clerk",
] as const;
export type ServiceName = (typeof SERVICE_NAME_LIST)[number];
export const SERVICE_NAMES: readonly ServiceName[] = SERVICE_NAME_LIST;
```

`ServiceName` is a TypeScript union literal type. `SERVICE_NAMES` is the runtime array that `inferServicesFromConfig` uses in `start.ts` to detect which services are mentioned in a seed file, and that `listCommand` iterates when printing the service table.

## How `startCommand` Drives the Registry

The startup sequence in `startCommand` follows a strict order that touches every part of each `ServiceEntry`:

```text
┌─────────────────────────────────────────────────────────┐
│  startCommand (start.ts)                                │
│                                                         │
│  1. Load seed config (YAML / JSON)                      │
│  2. Infer service list from config keys OR use all      │
│  3. For each ServiceName:                               │
│     a. entry.load()        → LoadedService              │
│     b. entry.defaultFallback(svcSeedConfig) → identity  │
│     c. resolveBaseUrl()    → URL string                 │
│     d. createServer(plugin, {tokens, fallbackUser})     │
│     e. loadedSvc.seedFromConfig(store, baseUrl, cfg)    │
│     f. serve({ fetch: app.fetch, port })                │
└─────────────────────────────────────────────────────────┘
```

Step (b) feeds the `ServiceEntry.defaultFallback` result directly into `createServer`. Step (e) only runs if both `svcSeedConfig` and `loadedSvc.seedFromConfig` are present — that is, the YAML has a section matching the service name, and the plugin package exported a seeding function.

Sources: [packages/emulate/src/commands/start.ts:133-192]().

## Base URL Resolution

Once a service is loaded, its publicly advertised URL is resolved through a five-step fallback chain defined in `base-url.ts`:

1. Per-service `baseUrl` key inside the seed config section
2. `--base-url` CLI flag (or programmatic option)
3. `EMULATE_BASE_URL` environment variable (supports `{service}` interpolation)
4. `PORTLESS_URL` environment variable (supports `{service}` interpolation)
5. `http://localhost:<port>` (default)

```typescript
// packages/emulate/src/base-url.ts:16-32
export function resolveBaseUrl(opts: ResolveBaseUrlOptions): string {
  if (opts.seedBaseUrl)  return opts.seedBaseUrl.replace(/\{service\}/g, opts.service);
  if (opts.baseUrl)      return opts.baseUrl.replace(/\{service\}/g, opts.service);
  const envBaseUrl = process.env.EMULATE_BASE_URL;
  if (envBaseUrl)        return envBaseUrl.replace(/\{service\}/g, opts.service);
  const portlessUrl = process.env.PORTLESS_URL;
  if (portlessUrl)       return portlessUrl.replace(/\{service\}/g, opts.service);
  return `http://localhost:${opts.port}`;
}
```

When `--portless` is used, `portlessBaseUrl(serviceName)` in `portless.ts` generates `https://<service>.emulate.localhost`, and `registerAliases` maps each `<service>.emulate` name to its localhost port via the `portless` CLI. Sources: [packages/emulate/src/portless.ts:91-93]() and [packages/emulate/src/portless.ts:66-80]().

## The `emulate list` Command

`listCommand` is the simplest consumer of the registry — it iterates `SERVICE_REGISTRY` and prints `entry.label` and `entry.endpoints` for each service:

```typescript
// packages/emulate/src/commands/list.ts:3-10
export function listCommand(): void {
  console.log("\nAvailable services:\n");
  for (const [name, entry] of Object.entries(SERVICE_REGISTRY)) {
    console.log(`  ${name.padEnd(10)}${entry.label}`);
    console.log(`            Endpoints: ${entry.endpoints}`);
  }
}
```

This means the output of `emulate list` is purely data-driven from the registry: adding a new service to `SERVICE_REGISTRY` automatically makes it appear in the CLI help output.

## `DEFAULT_TOKENS`

The registry also exports `DEFAULT_TOKENS`, a static object prepended by `initCommand` to every generated config file. It provides two ready-to-use bearer tokens — `test_token_admin` (mapped to `admin`) and `test_token_user1` (mapped to `octocat`) — so a project can make authenticated API calls immediately after `emulate init` without defining any tokens:

```typescript
// packages/emulate/src/registry.ts:507-518
export const DEFAULT_TOKENS = {
  tokens: {
    test_token_admin: {
      login: "admin",
      scopes: ["repo", "user", "admin:org", "admin:repo_hook"],
    },
    test_token_user1: {
      login: "octocat",
      scopes: ["repo", "user"],
    },
  },
};
```

## Summary

`registry.ts` acts as the single source of truth for every service emulate supports. Its `SERVICE_REGISTRY` record makes three commands — `start`, `init`, and `list` — purely data-driven: `start` calls `load()` and `defaultFallback()` at runtime, `init` serializes `initConfig` to YAML, and `list` reads `label` and `endpoints` for display. Adding a new service requires only a new `ServiceEntry` in this one file; no other command code needs to change. Sources: [packages/emulate/src/registry.ts:34-505]().
