# Telling Emulate What World to Create — Seed Config

> How the YAML/JSON seed file pre-populates users, repos, OAuth clients, messages, and tokens before any test runs, and why this makes tests deterministic without touching the real internet.

- 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

- `emulate.config.example.yaml`
- `packages/emulate/src/commands/start.ts`
- `packages/emulate/src/commands/init.ts`
- `packages/emulate/src/registry.ts`

---

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

- [emulate.config.example.yaml](emulate.config.example.yaml)
- [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/registry.ts](packages/emulate/src/registry.ts)
- [packages/@emulators/github/src/index.ts](packages/@emulators/github/src/index.ts)
- [packages/@emulators/google/src/index.ts](packages/@emulators/google/src/index.ts)
</details>

# Telling Emulate What World to Create — Seed Config

When you run `emulate start`, the emulator boots up knowing nothing: no users, no repos, no tokens, no messages. The **seed config file** is how you hand it a ready-made world before any test code runs. Think of it as the stage crew that sets the props before the actors arrive — once the server is up, every API call your app makes will find exactly the data you described, every time, without touching the real internet.

This page explains the shape of that file, how `emulate` finds and loads it, how each service's data is written into the in-memory store, and what defaults the emulator falls back to when no config is present.

---

## Where the Config File Lives

When `emulate start` is called without an explicit `--seed` flag, it walks through a fixed lookup list in priority order:

```
emulate.config.yaml
emulate.config.yml
emulate.config.json
service-emulator.config.yaml
service-emulator.config.yml
service-emulator.config.json
```

The first matching file wins. If you pass `--seed path/to/my-file.yaml` (or `.json`), that path is resolved and loaded directly instead.

The file format is detected from the extension: `.json` uses `JSON.parse`; everything else is parsed as YAML.

Sources: [packages/emulate/src/commands/start.ts:31-71]()

---

## Generating a Starter Config

The `emulate init` command writes a `emulate.config.yaml` in your current directory by reading each service's `initConfig` out of the registry and serialising it back to YAML:

```bash
npx emulate init              # generates config for all services
npx emulate init --service github
```

Each service in the registry carries an `initConfig` property that is a representative example for that service. `init` merges `DEFAULT_TOKENS` (the top-level `tokens` block) with whichever service configs you requested and writes the result.

Sources: [packages/emulate/src/commands/init.ts:10-38](), [packages/emulate/src/registry.ts:507-518]()

---

## Top-Level Structure

A seed config is a plain YAML (or JSON) document with two kinds of top-level keys:

| Key | Purpose |
|-----|---------|
| `tokens` | Static bearer tokens pre-mapped to user logins and OAuth scopes |
| `github`, `vercel`, `google`, `slack`, … | Per-service fixture data |

```yaml
# emulate.config.example.yaml (top)
tokens:
  gho_test_token_admin:
    login: admin
    scopes: [repo, user, admin:org, admin:repo_hook]
  gho_test_token_octocat:
    login: octocat
    scopes: [repo, user, read:user, "user:email", workflow]

github:
  users: [...]
  repos: [...]
  ...
```

Sources: [emulate.config.example.yaml:1-8]()

The emulator also uses which top-level service keys exist to **auto-select which services to start**. If your file has only a `github:` key, only the GitHub emulator boots; if it has `github:` and `google:`, both boot. You can override this with `--service github,google` on the command line.

Sources: [packages/emulate/src/commands/start.ts:74-98]()

---

## How Tokens Work

The `tokens` block is a map from token string → `{ login, scopes[] }`. At startup, `start.ts` reads this block and assigns each entry an incrementing numeric ID (starting at 100). Any HTTP call that carries one of these strings as a `Bearer` or `token` header is immediately authenticated as the mapped user.

```typescript
// start.ts:107-115
const tokens: Record<string, { login: string; id: number; scopes?: string[] }> = {};
if (seedConfig?.tokens) {
  let tokenId = 100;
  for (const [token, user] of Object.entries(seedConfig.tokens)) {
    tokens[token] = { login: user.login, id: tokenId++, scopes: user.scopes };
  }
} else {
  tokens["test_token_admin"] = { login: "admin", id: 2, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
}
```

If no `tokens` block is present, the emulator installs a single built-in `test_token_admin` → `admin` entry so you always have at least one working credential.

Sources: [packages/emulate/src/commands/start.ts:107-115]()

The `DEFAULT_TOKENS` exported from the registry provides the canonical example used by `emulate init`:

```typescript
// 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"] },
  },
};
```

Sources: [packages/emulate/src/registry.ts:507-518]()

---

## How Seed Data Reaches the Store

For each service, `start.ts` calls two seeding steps in sequence:

```typescript
// start.ts:184-188
loadedSvc.plugin.seed?.(store, baseUrl);               // 1. plugin built-in defaults

if (svcSeedConfig && loadedSvc.seedFromConfig) {
  loadedSvc.seedFromConfig(store, baseUrl, svcSeedConfig, webhooks); // 2. your config
}
```

Step 1 lets a plugin set up anything it always needs (e.g., system-level Gmail labels). Step 2 is where your YAML fixtures are written into the in-memory store. Each service package exports its own `seedFromConfig` function that knows the exact shape of its sub-section.

Sources: [packages/emulate/src/commands/start.ts:184-188]()

### The `LoadedService` interface

The contract every service must satisfy:

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

`seedFromConfig` is optional — a service that needs no pre-population can omit it.

Sources: [packages/emulate/src/registry.ts:1-7]()

---

## Per-Service Seed Shape

### GitHub

```yaml
github:
  users:
    - login: octocat
      name: The Octocat
      email: octocat@github.com
      bio: I am the Octocat
      company: GitHub
      location: San Francisco
  orgs:
    - login: my-org
      name: My Organization
  repos:
    - owner: octocat
      name: hello-world
      language: JavaScript
      auto_init: true          # seeds an initial commit, tree, branch, and ref
  oauth_apps:
    - client_id: emu_github_client_id
      client_secret: emu_github_client_secret
      name: Code App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github
  apps:
    - app_id: 12345
      slug: my-github-app
      private_key: |
        -----BEGIN RSA PRIVATE KEY-----
        ...
      installations:
        - installation_id: 100
          account: octocat
          repository_selection: all
```

When `auto_init: true` is set on a repo, `seedFromConfig` also creates an initial commit record, a root tree, a branch, and a ref so the repo looks freshly initialised without requiring any Git operations.

The full `GitHubSeedConfig` interface supports: `users`, `orgs`, `repos`, `oauth_apps`, `apps` (with nested `installations`), and `tokens`.

Sources: [packages/@emulators/github/src/index.ts:30-85](), [emulate.config.example.yaml:9-53]()

### Google (Gmail, Calendar, Drive)

```yaml
google:
  users:
    - email: testuser@gmail.com
      name: Test User
  oauth_clients:
    - client_id: emu_google_client_id
      client_secret: emu_google_client_secret
      redirect_uris:
        - http://localhost:3000/api/auth/callback/google
  labels:
    - id: Label_ops
      user_email: testuser@gmail.com
      name: Ops/Review
      color_background: "#DDEEFF"
  messages:
    - id: msg_welcome
      user_email: testuser@gmail.com
      from: welcome@example.com
      to: testuser@gmail.com
      subject: Welcome to the Gmail emulator
      label_ids: [INBOX, UNREAD, CATEGORY_UPDATES]
      date: 2025-01-04T10:00:00.000Z
  calendars:
    - id: primary
      user_email: testuser@gmail.com
      primary: true
      time_zone: UTC
  calendar_events:
    - id: evt_kickoff
      user_email: testuser@gmail.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@gmail.com
      name: Docs
      mime_type: application/vnd.google-apps.folder
      parent_ids: [root]
```

For each user in `users`, `seedFromConfig` also calls `ensureSystemLabels` to insert the standard Gmail system labels (INBOX, SENT, DRAFT, SPAM, TRASH, etc.) automatically. You do not need to declare those yourself.

Sources: [packages/@emulators/google/src/index.ts:294-355](), [emulate.config.example.yaml:80-130]()

### Other Services at a Glance

| Service key | Key fixture sections |
|-------------|----------------------|
| `vercel` | `users`, `teams`, `projects`, `integrations` |
| `slack` | `team`, `users`, `channels`, `bots`, `oauth_apps` |
| `apple` | `users`, `oauth_clients` |
| `microsoft` | `users`, `oauth_clients` |
| `okta` | `users`, `groups`, `authorization_servers`, `oauth_clients` |
| `aws` | `region`, `s3.buckets`, `sqs.queues`, `iam.users`, `iam.roles` |
| `resend` | `domains`, `contacts` |
| `stripe` | `customers`, `products`, `prices` |
| `mongoatlas` | `projects`, `clusters`, `database_users`, `databases` |
| `clerk` | `users`, `organizations`, `oauth_applications` |

All of these follow the same pattern: declare the data in YAML, and `seedFromConfig` upserts it into the in-memory store before any HTTP traffic arrives.

Sources: [packages/emulate/src/registry.ts:34-504]()

---

## Deduplication and Upsert Semantics

`seedFromConfig` implementations use a consistent upsert pattern: check whether the record already exists by a natural key, then insert or update:

```typescript
// github/src/index.ts (representative excerpt)
const existing = gh.users.findOneBy("login", user.login);
if (existing) {
  gh.users.update(existing.id, { ...fields });
} else {
  gh.users.insert({ ...fields });
}
```

This means re-running the emulator with the same config is safe and idempotent — no duplicate records accumulate.

Sources: [packages/@emulators/github/src/index.ts:135-373]()

---

## Fallback User (Unauthenticated Requests)

Each service entry in the registry defines a `defaultFallback` function. This determines which user identity is assumed when an API request arrives with no valid token. The fallback reads from the `svcSeedConfig` if available, otherwise uses a hard-coded default:

```typescript
// registry.ts — GitHub entry
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"] };
},
```

For most services, the fallback is the first user declared in your seed config. This keeps unauthenticated test calls (e.g., `fetch('/api/user')` without a token) pointing at a real seeded user rather than an anonymous phantom.

Sources: [packages/emulate/src/registry.ts:86-89](), [packages/emulate/src/commands/start.ts:172-179]()

---

## Startup Sequence: How It All Fits Together

```text
emulate start
│
├─ loadSeedConfig()          reads YAML/JSON from --seed or auto-detected file
│
├─ inferServicesFromConfig()  decides which services to boot from top-level keys
│
├─ tokens block              maps token strings → { login, id, scopes }
│
├─ for each service:
│   ├─ entry.load()           dynamic-imports the service package
│   ├─ createServer(plugin, { tokens, fallbackUser, ... })
│   ├─ plugin.seed(store)     built-in defaults (system Gmail labels, etc.)
│   └─ seedFromConfig(store, baseUrl, svcSeedConfig)
│       └─ upserts users, repos, messages, tokens, OAuth clients, …
│
└─ serve(fetch, port)         HTTP server is live with pre-populated data
```

Sources: [packages/emulate/src/commands/start.ts:79-210]()

---

## Why This Matters for Test Determinism

Because all data is written into an in-memory store at startup — before any test request fires — every test run starts from the exact same baseline. There is no shared state leaking between sessions, no race conditions with external APIs, and no network calls to GitHub, Google, or any real provider. Your fixtures are the ground truth, and the emulator enforces it.

When a test run finishes, calling `store.reset()` (which `shutdown` does automatically on SIGINT/SIGTERM) wipes everything, ensuring the next run starts clean. The seed config is replayed from scratch on the next `emulate start`.

Sources: [packages/emulate/src/commands/start.ts:196-210]()
