Provider Interfaces
Studio accesses all external services through provider interfaces. This page documents every interface, its purpose, current implementation, and how to create a custom provider.
Architecture
server/providers/
├── auth.ts # AuthProvider interface
├── database.ts # DatabaseProvider interface
├── git.ts # GitProvider + GitAppProvider interfaces
├── ai.ts # AIProvider interface
├── cdn.ts # CDNProvider interface
├── media.ts # MediaProvider interface
├── email.ts # EmailProvider interface
├── connector.ts # ConnectorProvider interface
├── payment/ # PaymentProvider interface + plugin registry
│ ├── types.ts # PaymentProvider + plugin contract
│ ├── registry.ts # plugin registry + default resolution
│ ├── index.ts # bootstrap + public API
│ └── plugins/ # polar.ts (default), stripe.ts (legacy)
├── supabase-auth.ts # AuthProvider implementation
├── supabase-client.ts # Shared Supabase client
├── supabase-db/ # DatabaseProvider implementation
├── github-app.ts # GitProvider + GitAppProvider implementation
├── anthropic-ai.ts # AIProvider implementation
├── resend-email.ts # EmailProvider implementation
└── index.ts # Re-exportsAll providers are resolved via singleton factories in server/utils/providers.ts.
AuthProvider
Handles token validation, OAuth flows, magic links, and user management.
Current implementation: Supabase Auth (supabase-auth.ts)
interface AuthUser {
id: string
email: string | null
avatarUrl: string | null
provider: 'github' | 'google' | 'email' | null
providerAccountId: string | null
}
interface AuthTokens {
accessToken: string
refreshToken: string | null
expiresAt: number // Unix timestamp in seconds
}
interface AuthSession {
user: AuthUser
tokens: AuthTokens
// Provider-side OAuth tokens (e.g. GitHub gho_*/ghu_*), captured at
// exchange time only. Null for magic-link / non-OAuth flows. The caller
// persists these via DatabaseProvider; the AuthProvider does not.
providerTokens?: ProviderTokens | null
}
interface AuthProvider {
validateToken(accessToken: string): Promise<AuthUser | null>
refreshSession(refreshToken: string): Promise<AuthTokens | null>
// Refresh an expired OAuth provider token (e.g. GitHub user-to-server token).
refreshProviderToken(provider: 'github' | 'google', refreshToken: string): Promise<ProviderTokens | null>
getOAuthRedirectUrl(provider: 'github' | 'google', redirectTo: string): Promise<OAuthRedirectResult>
exchangeCode(code: string, state?: string): Promise<AuthSession>
exchangeTokens(accessToken: string, refreshToken?: string): Promise<AuthSession>
sendMagicLink(email: string, redirectTo: string): Promise<void>
inviteUserByEmail(email: string, options?: { redirectTo?: string }): Promise<{ userId: string }>
getUserById(userId: string): Promise<AuthUser | null>
getUserByEmail(email: string): Promise<AuthUser | null>
deleteUser(userId: string): Promise<void>
}Session Management
Session management (encrypted cookies, refresh orchestration) lives in server/utils/session.ts, not in the AuthProvider. The provider is responsible only for token operations.
DatabaseProvider
The largest provider interface. Handles all persistent state: profiles, workspaces, projects, members, conversations, messages, media, forms, webhooks, CDN, and audit logs.
Current implementation: Supabase PostgreSQL with RLS (supabase-db/)
The interface is organized into sections:
| Section | Key Methods |
|---|---|
| Profiles | getProfile, updateProfile |
| Workspaces | listUserWorkspaces, createWorkspace, getWorkspaceForUser, updateWorkspace, deleteWorkspace |
| Workspace Members | listWorkspaceMembers, createWorkspaceMember, updateWorkspaceMemberRole, deleteWorkspaceMember, acceptPendingInvitations |
| Projects | getProjectForWorkspace, createProject, updateProject, deleteProject, listWorkspaceProjects |
| Project Members | listProjectMembers, getProjectMember, createProjectMember, deleteProjectMember |
| AI Keys | listUserAIKeys, upsertUserAIKey, deleteUserAIKey, getBYOAKey |
| Conversations | createConversation, getConversation, listConversations, deleteConversation |
| Messages | loadConversationMessages, insertMessage |
| Agent Usage | getAgentUsage, upsertAgentUsage, incrementAgentUsageIfAllowed, updateAgentUsageTokens |
| Media Assets | createMediaAsset, getMediaAsset, listMediaAssets, updateMediaAsset, deleteMediaAsset |
| Media Usage | trackMediaUsage, removeMediaUsage, getMediaUsage |
| Form Submissions | createFormSubmission, listFormSubmissions, updateFormSubmissionStatus, createFormSubmissionIfAllowed |
| Webhooks | createWebhook, listProjectWebhooks, updateWebhook, deleteWebhook |
| Webhook Deliveries | createWebhookDelivery, listWebhookDeliveries, updateWebhookDelivery |
| CDN Keys | validateCDNKeyHash, createCDNKey, listCDNKeys, revokeCDNKey |
| CDN Builds | createCDNBuild, updateCDNBuild, listCDNBuilds |
| Conversation Keys | validateConversationKeyHash, createConversationKey, revokeConversationKey |
| Audit Logs | createAuditLog |
WARNING
Every database query MUST scope by workspace_id, not just project_id. This is enforced at the RLS level in Supabase.
GitProvider
Handles all Git operations scoped to a specific repository. Unlike other providers, GitProvider is created per-repository, not as a singleton.
Current implementation: GitHub App (github-app.ts)
interface GitProvider {
// Tree operations
getTree(ref?: string): Promise<TreeEntry[]>
readFile(path: string, ref?: string): Promise<string>
listDirectory(path: string, ref?: string): Promise<string[]>
fileExists(path: string, ref?: string): Promise<boolean>
// Branch operations
createBranch(name: string, fromRef?: string): Promise<void>
listBranches(prefix?: string): Promise<Branch[]>
getBranchDiff(branch: string, base?: string): Promise<FileDiff[]>
mergeBranch(branch: string, into: string): Promise<MergeResult>
deleteBranch(branch: string): Promise<void>
isMerged(branch: string, into?: string): Promise<boolean>
// Commit operations
commitFiles(branch: string, files: FileChange[], message: string,
author: CommitAuthor): Promise<Commit>
// PR operations
createPR(head: string, base: string, title: string, body: string): Promise<{ id: string, url: string }>
mergePR(id: string): Promise<void>
// Permissions & config
getPermissions(): Promise<RepoPermissions>
getBranchProtection(branch: string): Promise<BranchProtection | null>
getDefaultBranch(): Promise<string>
// Detection
detectFramework(): Promise<FrameworkDetection>
}INFO
GitProvider extends the MCP RepoProvider contract (from @contentrain/types) with Studio-specific extensions. The Studio factory (createStudioGitProvider in git.ts) wraps MCP's GitHubProvider over a shared Octokit client. The canonical write method is applyPlan(input); commitFiles(...) is a backward-compatibility shim that delegates to it.
Usage:
const git = useGitProvider({
installationId: workspace.github_installation_id,
owner: 'org',
repo: 'my-repo',
})GitAppProvider
Manages GitHub App installation-level operations (not repository-specific).
interface GitAppProvider {
getInstallationDetails(): Promise<InstallationDetails>
listInstallationRepositories(): Promise<InstallationRepository[]>
createRepositoryFromTemplate(input: TemplateRepositoryInput): Promise<InstallationRepository>
canAccessRepository(owner: string, repo: string): Promise<boolean>
// Revoke (uninstall) the GitHub App from the bound account/org.
// Returns true on success (and on idempotent 404 "already gone").
revokeInstallation(): Promise<boolean>
}AIProvider
Abstracts AI model interaction for chat with tool use.
Current implementation: Anthropic Claude (anthropic-ai.ts)
interface AIProvider {
streamCompletion(request: AICompletionRequest, apiKey: string): AsyncGenerator<AIStreamEvent>
createCompletion(request: AICompletionRequest, apiKey: string): Promise<AICompletionResponse>
}
interface AICompletionRequest {
model: string
// String = a single uncached system block. Array form lets callers
// place prompt-cache breakpoints between blocks (up to 4 per request).
system: string | AISystemBlock[]
messages: AIMessage[]
tools: AITool[]
maxTokens: number
abortSignal?: AbortSignal
}
interface AIUsage {
inputTokens: number
outputTokens: number
cacheCreationInputTokens: number
cacheReadInputTokens: number
}
interface AIStreamEvent {
type: 'text' | 'tool_use_start' | 'tool_use_input' | 'tool_use_end' | 'message_end' | 'error'
content?: string
toolId?: string
toolName?: string
toolInput?: unknown
stopReason?: 'end_turn' | 'tool_use' | 'max_tokens'
usage?: AIUsage
error?: string
}CDNProvider
Abstracts object storage for CDN content delivery.
Current implementation: Cloudflare R2 (ee/cdn/cloudflare-cdn.ts)
interface CDNProvider {
putObject(projectId: string, path: string, data: string | Buffer, contentType: string): Promise<CDNObject>
getObject(projectId: string, path: string): Promise<{ data: Buffer, contentType: string, etag: string } | null>
deleteObject(projectId: string, path: string): Promise<void>
deletePrefix(projectId: string, prefix: string): Promise<void>
listObjects(projectId: string, prefix?: string): Promise<CDNObject[]>
purgeCache(projectId: string, paths?: string[]): Promise<void>
getStorageKey(projectId: string, path: string): string
}MediaProvider
Manages media upload, processing, variant generation, and metadata.
Current implementation: Sharp + R2 (ee/media/sharp-processor.ts)
interface MediaProvider {
upload(options: UploadOptions): Promise<MediaAsset>
regenerateVariants(assetId: string, variants: Record<string, VariantConfig>): Promise<MediaAsset>
delete(projectId: string, assetId: string): Promise<void>
getAsset(assetId: string): Promise<MediaAsset | null>
listAssets(projectId: string, options?: MediaListOptions): Promise<{ assets: MediaAsset[], total: number }>
updateMetadata(assetId: string, metadata: { alt?: string, tags?: string[], focalPoint?: { x: number, y: number } }): Promise<MediaAsset>
trackUsage(assetId: string, usage: MediaUsageRef): Promise<void>
removeUsage(assetId: string, usage: MediaUsageRef): Promise<void>
}EmailProvider
Sends application-level emails (invites, notifications). Auth emails are handled by Supabase SMTP.
Current implementation: Resend (resend-email.ts)
interface EmailProvider {
sendEmail(options: EmailSendOptions): Promise<void>
}
interface EmailSendOptions {
to: string
subject: string
html: string
from?: string
}PaymentProvider
Abstracts payment/subscription management. The interface is provider-agnostic; concrete plugins live under payment/plugins/ and self-register in the registry. The active plugin is resolved by preference order (polar -> stripe); when both are configured Polar wins, and when none is configured Studio runs in no-billing mode.
Current implementations: Polar (payment/plugins/polar.ts, default) + Stripe (payment/plugins/stripe.ts, legacy)
interface PaymentProvider {
createCheckoutSession(input: CheckoutInput): Promise<CheckoutResult>
createPortalSession(input: PortalInput): Promise<PortalResult>
// `headers` carries the raw request headers; each plugin picks the
// signature/timestamp headers it needs (Stripe: `stripe-signature`;
// Polar / Standard Webhooks: `webhook-signature` + `webhook-timestamp` + `webhook-id`).
handleWebhook(payload: string, headers: Record<string, string | undefined>): Promise<WebhookResult>
cancelSubscription(subscriptionId: string): Promise<void>
// Record a usage event for metered/overage billing. Polar ingests to a
// meter via its events API; Stripe logs a warning (no real-time metering).
ingestUsageEvent(input: UsageEventInput): Promise<void>
}handleWebhook returns a WebhookResult whose event is a canonical name (subscription.created / subscription.updated / subscription.canceled / invoice.paid / invoice.payment_failed / noop); each plugin maps its native events to these values.
Adding a Payment Provider
Implement PaymentProviderPlugin (with isConfigured() + create()) in a new file under payment/plugins/, then add a registerPlugin(...) line in payment/index.ts. No core code changes.
ConnectorProvider
Bridges external AI/design tools into Studio's content pipeline. Not yet implemented.
interface ConnectorProvider {
id: string
name: string
icon: string
auth: 'oauth2' | 'api_key' | 'none'
featureKey: string
authorize?(workspaceId: string, redirectUri: string): Promise<{ redirectUrl: string }>
browse(token: string, query?: string): Promise<ConnectorItem[]>
fetch(token: string, itemId: string): Promise<ConnectorContent>
}Creating a Custom Provider
To add a new provider implementation:
- Create a file in
server/providers/(e.g.,clerk-auth.ts) - Implement the full interface
- Update the factory in
server/utils/providers.ts:
import { createClerkAuthProvider } from '../providers/clerk-auth'
export function useAuthProvider(): AuthProvider {
if (!_authProvider)
_authProvider = createClerkAuthProvider() // Swap this line
return _authProvider
}- Zero application code changes required -- all routes, composables, and components continue working.
TIP
Providers that return null (CDN, Media, Email, Payment) enable graceful degradation. Features that depend on them simply become unavailable without errors.
Related Pages
- Architecture -- provider pattern overview
- Enterprise Edition -- CDN and Media implementations in
ee/ - Self-Hosting -- configuring provider dependencies