Integrations
Wire real providers: Stripe, HubSpot, Resend, Cloudflare Email, Supabase.
Wire ConversoKit to external services (payments, CRMs, calendars, …).
How an integration works
Each integration is a small interface in @conversokit/integrations plus one or more implementations:
// packages/integrations/src/types.ts
interface PaymentProvider {
id: string;
createCheckoutSession(opts): Promise<CheckoutSession>;
}
// packages/integrations/src/stripe.ts
class StripeProvider implements PaymentProvider { ... }
Consumers (MCP tools, CLI templates) import the interface and pick an implementation at runtime, usually keyed off env vars:
import { MockPaymentProvider, createStripeProvider } from '@conversokit/integrations';
const stripe = createStripeProvider(process.env);
const provider = stripe ?? new MockPaymentProvider();
This is the fallback pattern. Set the integration's env keys and you get the real provider; leave them unset and you get a Mock that keeps the dev loop unblocked.
What ships in 0.1.x
| Domain | Real providers | Mock / fallback |
|---|---|---|
| Payments | StripeProvider (Stripe Checkout + webhooks + idempotency) | MockPaymentProvider |
| CRM | HubspotProvider (CRM v3 create-or-PATCH on email conflict) | MockCrmProvider |
ResendEmailProvider, CloudflareEmailProvider (beta) | MockEmailProvider | |
| Persistent stores | SupabaseCartStore / OrderStore / ReservationStore / LeadStore / UserDataStore | InMemory* defaults in apps/mcp-server/src/store/ |
| Auth — OAuth flow | GoogleOAuthProvider, GitHubOAuthProvider, MicrosoftOAuthProvider, Auth0Provider | factory → null when env missing |
| Auth — JWT-verify | bearerJwtProvider (HS256 / JWKS), clerkAuthProvider (JWKS), supabaseAuthProvider (HS256) | — |
Each factory follows the same shape: create<Provider>(env) returns null when its env keys are missing, so consumers write real ?? mock.
JWT-verify providers (Clerk, Supabase Auth)
bearerJwtProvider({ secret? | jwksUri?, issuer?, audience? }) is the underlying primitive — it verifies bearer tokens from the Authorization header against either an HS256 shared secret or a remote JWKS.
- Clerk —
clerkAuthProvider(env)derives the Clerk frontend API host fromCLERK_PUBLISHABLE_KEY(or override viaCLERK_FRONTEND_API) and verifies tokens againsthttps://<frontendApi>/.well-known/jwks.jsonwith the matching issuer. - Supabase Auth —
supabaseAuthProvider(env)readsSUPABASE_JWT_SECRETfor HS256 verification; ifSUPABASE_URLis also set, the issuer is pinned to${SUPABASE_URL}/auth/v1.
Both are null when their env is unset — wire them into createAuthMiddleware({ providers }) alongside the other providers and the chain just skips them.
Stripe
# .env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
create_checkoutMCP tool builds a Stripe Checkout session, returns the URL.POST /webhooks/stripeverifies the signature and records orders inOrderStore(in-memory; swap for a DB-backed store before production).- Idempotency key =
${sessionId}:${cartHash}.
To test webhooks locally:
stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger checkout.session.completed
curl localhost:3000/admin/orders
Tracking and attribution
Commerce-style apps often need to connect a ChatGPT conversation to downstream clicks, cart actions, checkout redirects, and sales. Keep this first-party and explicit:
import {
setConversationId,
track,
trackOutboundClick,
trackConversion,
} from '@/lib/analytics';
setConversationId(ctx.sessionId);
track('add_to_cart', {
props: { product_id: product.id, quantity },
});
const checkoutUrl = trackOutboundClick(stripeCheckout.url, {
props: { destination: 'stripe_checkout' },
});
trackConversion('sale_completed', {
conversionId: order.id,
value: order.total,
currency: 'USD',
});
The helper captures UTM and paid-click identifiers (gclid, fbclid, msclkid,
ttclid), appends safe identifiers such as ck_aid and ck_cid to outbound URLs,
and stores analytics events without raw email, phone, IP, password, or token fields.
Adding a new integration
- Write the interface in
packages/integrations/src/<domain>.ts. - Implement at least one real provider and a mock fallback.
- Add
create<Provider>(env)factory that returnsnullwhen env is missing. - Re-export from
packages/integrations/src/index.ts. - Update the integration's MCP tool to use the fallback pattern above.
- Document the env keys in
.env.example. - Add it to the relevant template's
integrations: [...]field in@conversokit/templates.
Persistence (Supabase recipe)
The default stores under apps/mcp-server/src/store/* are InMemory* — fine for dev but lose data on restart. To swap for Supabase:
- Create a Supabase project; export
SUPABASE_URL+SUPABASE_SERVICE_ROLE_KEY. - Implement
CartStore,OrderStore,LeadStore,ReservationStoreagainst@supabase/supabase-jsin a newpackages/integrations/src/supabase.ts. - Replace the
defaultXStoreexports with environment-aware factories.
This lives outside MVP — the interfaces let you do it without touching tools.