Skip to content
Merged
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
235 changes: 235 additions & 0 deletions .agents/product-marketing.md

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# DomainKit

DomainKit is a provider-independent toolkit for connecting domains and provisioning DNS.
DomainKit helps SaaS teams build provider-connected domain setup into their products. It turns DNS
requirements into exact, reviewable plans and applies only an authorized digest while credentials,
persistence, and policy remain under application control.

Plans are additive and fail closed: missing records can be created, exact records are no-ops, and
incompatible state is reported as a conflict rather than overwritten.

## Packages

- [`domainkit`](./packages/domainkit/README.md) provides the Effect-native core and Promise facade.
- [`@domainkit/react`](./packages/react/README.md) provides browser-safe React flows over a host-owned transport.

The repository also contains the documentation application and an interactive React workshop under
[`apps/docs`](./apps/docs).
6 changes: 3 additions & 3 deletions apps/docs/blume.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default defineConfig({
output: "static",
site: "https://domain-kit.dev",
},
description: "Reviewable DNS provisioning for TypeScript, with React flows you host.",
description: "Domain setup infrastructure for SaaS, with reviewable DNS plans and React flows.",
github: {
dir: "apps/docs",
owner: "AryaLabsHQ",
Expand All @@ -33,13 +33,13 @@ export default defineConfig({
seo: {
og: {
titles: {
"/": "Reviewable DNS plans for TypeScript",
"/": "Build domain setup into your SaaS",
"/workshop": "Try DomainKit React DNS components",
},
},
},
theme: {
accent: "green",
accent: "blue",
mode: "system",
radius: "md",
},
Expand Down
36 changes: 26 additions & 10 deletions apps/docs/content/index.mdx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
---
title: Reviewable DNS plans
description: Connect domains with reviewable DNS plans. DomainKit authorizes an exact digest, applies it through your provider, and never silently overwrites records.
title: Domain setup infrastructure
description: Build provider-connected domain setup into your SaaS with exact DNS plans, host-owned credentials, and React flows you control.
sidebar:
label: Overview
order: 1
seo:
title: DNS provisioning SDK with reviewable plans
title: Domain setup infrastructure for SaaS
---

DomainKit is a TypeScript SDK for connecting domains and turning DNS requirements into reviewable, authorized plans.
DomainKit is a TypeScript toolkit for building provider-connected domain setup into a SaaS product.
Your application defines the DNS requirements and owns the customer, credential, persistence, and
policy boundaries. DomainKit supplies the portable connection, planning, authorization,
verification, and cleanup lifecycle.

The package root is Effect-native. A secondary Promise facade is available for foreign runtime boundaries. `@domainkit/react` provides browser-safe connection, provisioning, verification, cleanup, and DNS record interfaces over a transport owned by the host application.
The package root is Effect-native. A secondary Promise facade is available for foreign runtime
boundaries. `@domainkit/react` provides browser-safe connection, provisioning, verification,
cleanup, and DNS record interfaces over a transport owned by the host application.

```package-install
domainkit
Expand All @@ -29,15 +34,26 @@ domainkit
<Card title="React UI" href="/docs/react" icon="component">
Use complete flows or compose semantic parts into your own product.
</Card>
<Card title="Cloudflare" href="/docs/providers/cloudflare" icon="cloud">
<ProviderCard title="Cloudflare" href="/docs/providers/cloudflare" provider="cloudflare">
Connect with OAuth or scoped API tokens and discover the owning account.
</Card>
<Card title="Vercel" href="/docs/providers/vercel" icon="triangle">
</ProviderCard>
<ProviderCard title="Vercel" href="/docs/providers/vercel" provider="vercel">
Preserve personal or team context from a Vercel Integration installation.
</Card>
</ProviderCard>
</CardGroup>

## Principles
## Product boundary

| DomainKit owns | Your application owns |
| ------------------------------------------- | ------------------------------------------------------ |
| Provider-neutral DNS requirements and plans | Product-specific DNS requirements |
| Digest-bound plan authorization | Identity, tenancy, consent, and audit policy |
| Cloudflare and Vercel provider integrations | Credential encryption and durable persistence |
| Provider and public-DNS observation | Authenticated routes and product readiness state |
| Receipt-bound cleanup | When and how users may remove records |
| Browser-safe React lifecycle contracts | Branding, surrounding UI, notifications, and telemetry |

## Safety model

- **Review before mutation.** A plan lists creates, exact no-ops, and conflicts before any write.
- **Authorize the digest.** Apply accepts the reviewed plan plus authorization for that exact digest.
Expand Down
145 changes: 132 additions & 13 deletions apps/docs/content/providers/cloudflare.mdx
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
---
title: Cloudflare
description: Connect Cloudflare with OAuth or a scoped API token. DomainKit finds the account from a visible zone so customers do not type an account ID.
title: Connect Cloudflare
description: Connect a Cloudflare account with OAuth or a scoped API token, discover the account from a known zone, and persist the resulting authorization on your server.
seo:
title: Connect Cloudflare DNS with OAuth or tokens
title: Connect Cloudflare DNS with OAuth or API tokens
---

Cloudflare supports OAuth and API-token connection methods. Both produce the same durable DomainKit authorization aggregate and provider context.
<ProviderLogo provider="cloudflare" size={48} />

## OAuth
Cloudflare supports standards-based OAuth and API-token connection methods. Both produce the same
durable DomainKit authorization aggregate and provider context.

Run every step in this guide on a trusted server. Do not construct a Cloudflare provider client or
retain the token in browser code.

## Prerequisites

- A durable `AuthorizationLifecycle.Repository` for authorization aggregates and credentials.
- A short-lived, one-time continuation store for OAuth.
- Authenticated start and callback routes bound to your host user and tenant.
- A known customer domain whose authoritative zone the credential must be able to read.
- For OAuth, a registered Cloudflare client, redirect URI, client secret, and the scope IDs assigned
to that client.

## Start OAuth

```ts
import { Cloudflare, Connection, DomainName, Secret } from "domainkit/promise";
import {
Cloudflare,
Connection,
DnsRecord,
DomainName,
Secret,
Verification,
} from "domainkit/promise";

const capabilities = ["dns:read", "dns:write"] as const;

const flow = Cloudflare.Auth.oauthFlow({
capabilities: ["dns:read", "dns:write"],
capabilities,
client: {
clientId,
clientSecret: Secret.make(clientSecret),
Expand All @@ -26,23 +50,118 @@ const flow = Cloudflare.Auth.oauthFlow({

const result = await Connection.start({
authorizedById,
grant: { _tag: "account", excludedDomains: [] },
grant: {
_tag: "domains",
domains: [DomainName.parse("example.com")],
},
method: Connection.Method.Interactive({ continuations, flow }),
ownerId,
repository,
});
```

If the credential can see the requested domain, DomainKit resolves the Cloudflare account from the zone. The customer does not need to find or type an account ID.
An interactive start returns `Redirect`. Send its `authorizationUrl` to the browser only after the
host has checked the current session and tenant authority.

## API token
The flow uses the known domain to resolve the selected Cloudflare account after authorization. The
customer does not need to find or type an account ID.

## Complete the callback

Use the same flow configuration on the authenticated callback route:

```ts
const connected = await Connection.complete({
callbackUrl: new URL(request.url),
continuationId,
continuations,
flow,
repository,
});
```

`Connection.complete` consumes the continuation exactly once, validates the OAuth response,
discovers the account from the known zone, and commits the provider authorization. Persist only the
non-secret provider context in ordinary application records; the credential belongs behind the
repository's secret-storage boundary.

## Connect an API token

Use a token route when the customer supplies a scoped Cloudflare API token:

```ts
const domain = DomainName.parse("example.com");
const method = Cloudflare.Auth.tokenConnectionMethod({
capabilities: ["dns:read", "dns:write"],
domain: DomainName.parse("example.com"),
capabilities,
domain,
token: Secret.make(apiToken),
});

const connected = await Connection.start({
authorizedById,
grant: { _tag: "domains", domains: [domain] },
method,
ownerId,
repository,
});
```

Ask for the narrowest token that covers the intended zones and DNS permissions. Domain-targeted
validation supports user-owned and account-owned API tokens and discovers the account from the
visible zone.

## Reconstruct the provider

After loading and decoding the stored credential and provider context, construct the account-scoped
provider on the server:

```ts
const provider = Cloudflare.make({
accountId: authorization.providerAccountId,
capabilities: authorization.requiredCapabilities,
token: credential.accessToken,
tokenKind: context.tokenKind,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Provider context cannot be decoded

When a Promise API reader reconstructs an account-token provider from the persisted context, context.tokenKind is unavailable because the stored value is an envelope and the Promise facade exposes no provider-specific decoder. Reading the envelope directly either fails type checking or passes undefined, selecting the user-token verification endpoint and causing account-token authentication to fail.

Knowledge Base Used:

Fix in Codex Fix in Cursor Cloud Agents Fix in Claude Code Fix in Cursor

});
```

Pass that provider directly through the Promise APIs' `provider` fields for planning, apply, and
receipt-bound cleanup. Authoritative observation also needs the zone-tagged verification wrapper:

```ts
const zone = DomainName.parse("example.com");
const record = DnsRecord.parse({
_tag: "TXT",
metadata: {
ownership: "customer",
provenance: "example",
purpose: "domain verification",
},
name: "_verify.example.com",
policy: "append",
ttl: 300,
value: "verification-value",
});

await Verification.observe({
provider: Verification.Provider.Enabled({ provider, zone }),
Comment thread
greptile-apps[bot] marked this conversation as resolved.
record,
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
```

Ask for the narrowest token that covers the intended zones and DNS permissions. DomainKit records declared, introspected, or exercised capability evidence instead of treating scope strings as proof.
## Account discovery outcomes

- One matching visible zone identifies the Cloudflare account and can continue.
- No matching zone is an actionable authorization or zone-ownership failure.
- Ambiguous provider accounts require host/user selection; never choose an opaque account ID
silently.

Cloudflare's non-mutating token verification response does not always enumerate DNS permissions.
DomainKit records declared, introspected, and exercised capability evidence instead of treating
scope strings as complete proof.

## Next steps

- [Integrate the host lifecycle](/docs/guides/host-integration)
- [Provision and clean up records](/docs/guides/provision-and-clean-up)
- [Cloudflare capability reference](/docs/reference/providers#cloudflare)
- [Troubleshoot provider connections](/docs/guides/troubleshooting#connections)
104 changes: 97 additions & 7 deletions apps/docs/content/providers/vercel.mdx
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
---
title: Vercel
description: Connect Vercel through an Integration install or a token. DomainKit keeps the personal or team context Vercel returns for later requests.
title: Connect Vercel
description: Connect Vercel through an Integration installation or personal access token and preserve the personal or team context required for later DNS requests.
seo:
title: Connect Vercel DNS via Integration or token
title: Connect Vercel DNS through an Integration or token
---

Vercel's interactive method is an Integration installation-code flow. DomainKit models it as an integration rather than calling every redirect-based exchange OAuth.
<ProviderLogo provider="vercel" size={48} />

Vercel's interactive method is an Integration installation-code flow. DomainKit models it as an
integration rather than treating every redirect-based code exchange as generic OAuth.

Run provider exchanges and token validation on a trusted server. The browser receives only your
application's connection outcome.

## Prerequisites

- A Vercel Integration with a slug, client ID, client secret, and registered redirect URI.
- A durable `AuthorizationLifecycle.Repository`.
- A short-lived, one-time continuation store.
- Authenticated start and callback routes tied to the current host owner.

## Start an Integration installation

```ts
import { Connection, Secret, Vercel } from "domainkit/promise";

const capabilities = ["dns:read", "dns:write"] as const;

const flow = Vercel.Auth.integrationFlow({
capabilities: ["dns:read", "dns:write"],
capabilities,
clientId,
clientSecret: Secret.make(clientSecret),
redirectUri,
Expand All @@ -27,6 +44,79 @@ const result = await Connection.start({
});
```

The completed credential carries the personal-account or team context returned by Vercel. Persist that non-secret, versioned context with the authorization so later requests reconstruct the correct provider client without asking the customer for a team ID.
Send the returned `Redirect.authorizationUrl` to the browser. Vercel lets the user choose a personal
or team installation during this flow.

## Complete the callback

Use the same flow configuration on the callback route:

```ts
const connected = await Connection.complete({
callbackUrl: new URL(request.url),
continuationId,
continuations,
flow,
repository,
});
```

The code exchange returns the personal account or team context and installation identity. DomainKit
encodes that non-secret, versioned provider context with the authorization so later requests
reconstruct the correct Vercel client without asking the customer to type a team ID.

## Connect a personal access token

For a personal account:

```ts
const method = Vercel.Auth.tokenConnectionMethod({
capabilities,
context: { _tag: "personal" },
token: Secret.make(personalAccessToken),
});
```

For a team, provide the team context selected by the host:

```ts
const method = Vercel.Auth.tokenConnectionMethod({
capabilities,
context: { _tag: "team", teamId },
token: Secret.make(personalAccessToken),
});
```

Then pass the method to `Connection.start` with the owner grant and durable repository. Keep the
token behind the host's secret-storage boundary.

## Reconstruct the provider

Decode the stored context and construct the correct server-side client:

```ts
const provider = Vercel.make({
capabilities: authorization.requiredCapabilities,
context:
context._tag === "team" ? { _tag: "team", teamId: context.teamId } : { _tag: "personal" },
token: credential.accessToken,
});
```

Personal clients omit team context. Team clients attach `teamId` to Vercel resource requests.

## Zone discovery

DomainKit treats a Vercel domain as DNS storage when Vercel reports its DNS service type, zone flag,
or intended nameservers. Current or intended Vercel nameservers are provider evidence; they are not
substituted for independent public-DNS observation.

When more than one authorized context can serve a requested domain, surface a recognizable
personal/team choice instead of silently selecting a team ID.

## Next steps

Vercel tokens are also supported through `Vercel.Auth.tokenConnectionMethod`. Keep the token server-side and expose only the connection result through your application transport.
- [Integrate the host lifecycle](/docs/guides/host-integration)
- [Provision and clean up records](/docs/guides/provision-and-clean-up)
- [Vercel capability reference](/docs/reference/providers#vercel)
- [Troubleshoot provider connections](/docs/guides/troubleshooting#connections)
Loading
Loading