Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# Changelog

## 0.14.1
## 0.15.0

<!-- release:start -->

### New Features

- **Autumn `balances.update`** — the Autumn emulator now supports the SDK's balance update call (`POST /v1/balances.update`) for reconciling continuous-use features such as seats. Exactly one of `usage`, `remaining`, or `add_to_balance` is required; the update is recorded as an adjustment event, so `events.list` shows the reconciliation and `balances.check` and `customers.get_or_create` reflect it from the same state. `remaining` is rejected on unlimited balances, unknown customers 404 with Autumn's real `customer_not_found` code (update is a non-creating endpoint upstream, unlike track and check), and a feature the customer's plan does not carry 404s.
- **Polar billing emulator** (`@emulators/polar`, `npx emulate --service polar`) — a stateful emulator for the subscription and usage-based billing surface applications use through `@polar-sh/sdk`: customers with external ids (unique, deliverable-looking emails, Polar's exact 422 messages), meters with filters and aggregations, event ingestion (unknown external customers accepted and attributed once the customer exists), meter credit and custom benefits, recurring products with fixed or metered prices and trials, subscriptions (server-side creation for free products only, product changes with `next_period` pending updates, cancel at period end, revoke), customer state with active subscriptions, granted benefits, and meter balances, customer meters, hosted checkout (a free subscription upgraded in place via `subscription_id`, confirmation deferred until settled by `POST /checkout/:secret/settle` or an auto-settle delay), and customer portal sessions. Faults and the request ledger key on Polar's real operation ids (for example `customers:get_state_external`, `events:ingest`).

<!-- release:end -->

## 0.14.1


### New Features

- **Autumn `balances.update`** — the Autumn emulator now supports the SDK's balance update call (`POST /v1/balances.update`) for reconciling continuous-use features such as seats. Exactly one of `usage`, `remaining`, or `add_to_balance` is required; the update is recorded as an adjustment event, so `events.list` shows the reconciliation and `balances.check` and `customers.get_or_create` reflect it from the same state. `remaining` is rejected on unlimited balances, unknown customers 404 with Autumn's real `customer_not_found` code (update is a non-creating endpoint upstream, unlike track and check), and a feature the customer's plan does not carry 404s.


## 0.14.0

### New Features
Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ All services start with sensible defaults. No config file needed:
- **PostHog** on `http://localhost:4016`
- **MCP** on `http://localhost:4017`
- **GitLab** on `http://localhost:4018` (full real GraphQL schema)
- **Polar** on `http://localhost:4019` (subscriptions, usage metering, checkout, and customer portal)

Every running service also exposes a public control plane under `/_emulate`:

Expand Down Expand Up @@ -174,7 +175,7 @@ github:

## Deployed Instances

All services are available on host-based routing when deployed: `github`, `gitlab`, `mcp`, `vercel`, `google`, `okta`, `microsoft`, `spotify`, `slack`, `apple`, `aws`, `resend`, `stripe`, `mongoatlas`, `clerk`, `x`, `workos`, `autumn`, and `posthog`. Each one supports three addressing forms:
All services are available on host-based routing when deployed: `github`, `gitlab`, `mcp`, `vercel`, `google`, `okta`, `microsoft`, `spotify`, `slack`, `apple`, `aws`, `resend`, `stripe`, `mongoatlas`, `clerk`, `x`, `workos`, `autumn`, `posthog`, and `polar`. Each one supports three addressing forms:

```text
https://github.emulators.dev # service host (control plane only)
Expand Down Expand Up @@ -264,7 +265,7 @@ afterAll(() => Promise.all([github.close(), vercel.close()]));

| Option | Default | Description |
| --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service` | _(required)_ | Service name: `'vercel'`, `'github'`, `'gitlab'`, `'google'`, `'slack'`, `'apple'`, `'microsoft'`, `'okta'`, `'aws'`, `'resend'`, `'stripe'`, `'mongoatlas'`, `'clerk'`, `'spotify'`, `'x'`, `'workos'`, `'autumn'`, or `'posthog'` |
| `service` | _(required)_ | Service name: `'vercel'`, `'github'`, `'gitlab'`, `'google'`, `'slack'`, `'apple'`, `'microsoft'`, `'okta'`, `'aws'`, `'resend'`, `'stripe'`, `'mongoatlas'`, `'clerk'`, `'spotify'`, `'x'`, `'workos'`, `'autumn'`, `'posthog'`, `'mcp'`, or `'polar'` |
| `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>`. |
Expand Down Expand Up @@ -817,6 +818,31 @@ curl -s -X POST http://localhost:4018/api/graphql \

Because the full schema is real, this surface is well suited to testing GraphQL clients and generators against a large, production-shaped type system without calling gitlab.com. Use `/_emulate/manifest` for the declared coverage and `/_emulate/ledger` to inspect calls.

## Polar Billing API

Polar emulates the subscription and usage-based billing paths used by applications built with `@polar-sh/sdk`. It includes customers, meters, events, benefits, products, subscriptions, hosted checkout, and customer portal sessions.

```bash
npx emulate --service polar
```

When all services run together, Polar uses `http://localhost:4019`. Any non-empty bearer token is accepted.

```ts
import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
accessToken: "polar_oat_test",
serverURL: "http://localhost:4019",
});

const state = await polar.customers.getStateExternal({ externalId: "customer_123" });
```

Checkout confirmation deliberately leaves the new or upgraded subscription pending for a short interval. Set `checkout.settle_delay_ms` in seed data to control that interval, use `null` to disable automatic settlement, or call `POST /checkout/:clientSecret/settle` to make the subscription visible immediately.

The package serves its hand-authored API description at `GET /openapi.json`. Use `GET /_emulate/manifest` for declared coverage, `GET /_emulate/ledger` to inspect calls, and `POST /_emulate/faults` to inject failures by Polar operation ID.

## 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.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/docs/deployment/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Instances are created lazily. Creating one returns the base URL, control base UR

## Hosted services

The hosted catalog includes all 13 services: GitHub, Vercel, Google, Okta, Microsoft Entra ID, Spotify, Slack, Apple, AWS, Resend, Stripe, MongoDB Atlas, and Clerk.
The hosted catalog includes Vercel, GitHub, GitLab, Google, Slack, Apple, Microsoft Entra ID, Okta, AWS, Resend, Stripe, MongoDB Atlas, Clerk, Spotify, X, WorkOS, Autumn, PostHog, MCP, and Polar.

## Docs subdomain

Expand Down
9 changes: 7 additions & 2 deletions apps/web/app/docs/page.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Getting Started

Local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, AWS, Okta, MongoDB Atlas, Resend, and Stripe APIs. Built for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.
Local drop-in replacement for developer APIs including Vercel, GitHub, Google, Slack, Stripe, and Polar. Built for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.

## Quick Start

Expand All @@ -23,8 +23,13 @@ All services start with sensible defaults. No config file needed:
- **MongoDB Atlas** on `http://localhost:4010`
- **Clerk** on `http://localhost:4011`
- **Spotify** on `http://localhost:4012`
- **PostHog** on `http://localhost:4016`
- **X** on `http://localhost:4013`
- **WorkOS** on `http://localhost:4014`
- **Autumn** on `http://localhost:4015`
- **PostHog** on `http://localhost:4016`
- **MCP** on `http://localhost:4017`
- **GitLab** on `http://localhost:4018`
- **Polar** on `http://localhost:4019`

## Control Plane

Expand Down
7 changes: 7 additions & 0 deletions apps/web/app/docs/polar/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";

export const metadata = pageMetadata("polar");

export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
78 changes: 78 additions & 0 deletions apps/web/app/docs/polar/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Polar

Polar merchant-of-record billing emulation for subscription and usage-based billing integrations. The emulator supports the real request paths and response shapes used by `@polar-sh/sdk` 0.49.0.

## Start

```bash
npx emulate --service polar
```

When all services run together, Polar uses `http://localhost:4019`. Pass any non-empty bearer token.

```ts
import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
accessToken: "polar_oat_test",
serverURL: "http://localhost:4019",
});
```

## Supported billing flow

The API includes customers, meters, usage events, meter-credit and custom benefits, recurring products, subscriptions, checkouts, customer sessions, and an organization stub. Customer state calculates active subscriptions, granted benefits, and meter balances from the same stored entities.

Programmatic subscription creation accepts free products. Use `POST /v1/checkouts/` and the hosted `/checkout/:clientSecret` page for paid products and trials.

## Delayed checkout settlement

Confirming a hosted checkout redirects to the configured success URL but leaves the new subscription or upgrade pending. By default it settles after 2500 milliseconds when state is next read. Seed `checkout.settle_delay_ms` to change the delay, set it to `null` to disable automatic settlement, or call `POST /checkout/:clientSecret/settle` explicitly.

```yaml
polar:
checkout:
settle_delay_ms: null
```

## Seed data

Meters, benefits, and products are upserted by name or description. Customers are upserted by external ID. Product and benefit references may use an ID or their seed name.

```yaml
polar:
meters:
- name: API calls
filter:
conjunction: and
clauses:
- property: name
operator: eq
value: api.call
aggregation:
func: sum
property: count
benefits:
- type: meter_credit
description: 100 API calls
meter: API calls
units: 100
products:
- name: Free
recurring_interval: month
prices:
- amount_type: fixed
price_amount: 0
price_currency: usd
benefits:
- 100 API calls
customers:
- external_id: customer_123
email: customer@example.com
subscriptions:
- product: Free
```

## Inspect and fault

`GET /openapi.json` serves the hand-authored API description. Use `GET /_emulate/ledger` to inspect authenticated requests and side effects. Faults can target Polar operation IDs such as `customers:get_state_external` through `POST /_emulate/faults`.
10 changes: 9 additions & 1 deletion apps/web/app/docs/programmatic-api/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ afterAll(() => Promise.all([github.close(), vercel.close()]));
<td>
Service name: <code>'vercel'</code>, <code>'github'</code>, <code>'google'</code>, <code>'slack'</code>,{" "}
<code>'apple'</code>, <code>'microsoft'</code>, <code>'aws'</code>, <code>'okta'</code>,{" "}
<code>'mongoatlas'</code>, <code>'resend'</code>, or <code>'stripe'</code>
<code>'mongoatlas'</code>, <code>'resend'</code>, <code>'stripe'</code>, <code>'clerk'</code>,{" "}
<code>'spotify'</code>, <code>'x'</code>, <code>'workos'</code>, <code>'autumn'</code>, <code>'posthog'</code>,{" "}
<code>'mcp'</code>, <code>'gitlab'</code>, or <code>'polar'</code>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -243,6 +245,12 @@ npm install @emulators/github @emulators/google @emulators/stripe
</td>
<td>PostHog API, OpenAPI OAuth discovery, CIMD OAuth</td>
</tr>
<tr>
<td>
<code>@emulators/polar</code>
</td>
<td>Polar subscriptions, usage metering, hosted checkout, and customer portal</td>
</tr>
<tr>
<td>
<code>@emulators/core</code>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export default function LandingPage() {
Local API emulation for dev and CI
</h1>
<p className="mb-8 max-w-xl text-base text-neutral-600 dark:text-neutral-400">
Stateful, production-fidelity replacements for Stripe, GitHub, Google, AWS, and 7 more services. No API keys.
No network. Not mocks.
Stateful, production-fidelity replacements for Stripe, Polar, GitHub, Google, AWS, and more services. No API
keys. No network. Not mocks.
</p>

<div className="mb-6 flex flex-wrap items-center gap-3">
Expand Down Expand Up @@ -62,7 +62,7 @@ export default function LandingPage() {
<h3 className="mb-1 text-sm font-medium text-neutral-900 dark:text-neutral-100">Zero config</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Run <code className="rounded bg-neutral-100 px-1 py-0.5 text-xs dark:bg-neutral-800">npx emulate</code>{" "}
and all 11 services start with sensible defaults. Seed data via YAML when you need it.
and every service starts with sensible defaults. Seed data via YAML when you need it.
</p>
</div>
<div>
Expand Down
19 changes: 14 additions & 5 deletions apps/web/components/hero-terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ const services = [
{ name: "Slack", port: 4003, slug: "slack" },
{ name: "Apple", port: 4004, slug: "apple" },
{ name: "Microsoft", port: 4005, slug: "microsoft" },
{ name: "AWS", port: 4006, slug: "aws" },
{ name: "Okta", port: 4007, slug: "okta" },
{ name: "MongoDB Atlas", port: 4008, slug: "mongoatlas" },
{ name: "Resend", port: 4009, slug: "resend" },
{ name: "Stripe", port: 4010, slug: "stripe" },
{ name: "Okta", port: 4006, slug: "okta" },
{ name: "AWS", port: 4007, slug: "aws" },
{ name: "Resend", port: 4008, slug: "resend" },
{ name: "Stripe", port: 4009, slug: "stripe" },
{ name: "MongoDB Atlas", port: 4010, slug: "mongoatlas" },
{ name: "Clerk", port: 4011, slug: "clerk" },
{ name: "Spotify", port: 4012, slug: "spotify" },
{ name: "X", port: 4013, slug: "x" },
{ name: "WorkOS", port: 4014, slug: "workos" },
{ name: "Autumn", port: 4015, slug: "autumn" },
{ name: "PostHog", port: 4016, slug: "posthog" },
{ name: "MCP", port: 4017, slug: "mcp" },
{ name: "GitLab", port: 4018, slug: "gitlab" },
{ name: "Polar", port: 4019, slug: "polar" },
];

export function HeroTerminal({ pixelFont }: { pixelFont: string }) {
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/docs-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const allDocsPages: NavItem[] = [
{ name: "WorkOS", href: "/docs/workos" },
{ name: "Spotify", href: "/docs/spotify" },
{ name: "PostHog", href: "/docs/posthog" },
{ name: "Polar", href: "/docs/polar" },
{ name: "Authentication", href: "/docs/authentication" },
{ name: "Service Manifest", href: "/docs/manifest" },
{ name: "Request Ledger", href: "/docs/ledger" },
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/page-titles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const PAGE_TITLES: Record<string, string> = {
workos: "WorkOS",
spotify: "Spotify",
posthog: "PostHog",
polar: "Polar",
authentication: "Authentication",
manifest: "Service Manifest",
ledger: "Request Ledger",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/adapter-next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/adapter-next",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/apple/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/apple",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/autumn/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/autumn",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/aws/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/aws",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/clerk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/clerk",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/cloudflare",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/core",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/github/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/github",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/gitlab/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/gitlab",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/google/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/google",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/mcp",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/microsoft/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/microsoft",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/@emulators/mongoatlas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@emulators/mongoatlas",
"version": "0.14.1",
"version": "0.15.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
Expand Down
Loading