Skip to content

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-exports

All 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)

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:

SectionKey Methods
ProfilesgetProfile, updateProfile
WorkspaceslistUserWorkspaces, createWorkspace, getWorkspaceForUser, updateWorkspace, deleteWorkspace
Workspace MemberslistWorkspaceMembers, createWorkspaceMember, updateWorkspaceMemberRole, deleteWorkspaceMember, acceptPendingInvitations
ProjectsgetProjectForWorkspace, createProject, updateProject, deleteProject, listWorkspaceProjects
Project MemberslistProjectMembers, getProjectMember, createProjectMember, deleteProjectMember
AI KeyslistUserAIKeys, upsertUserAIKey, deleteUserAIKey, getBYOAKey
ConversationscreateConversation, getConversation, listConversations, deleteConversation
MessagesloadConversationMessages, insertMessage
Agent UsagegetAgentUsage, upsertAgentUsage, incrementAgentUsageIfAllowed, updateAgentUsageTokens
Media AssetscreateMediaAsset, getMediaAsset, listMediaAssets, updateMediaAsset, deleteMediaAsset
Media UsagetrackMediaUsage, removeMediaUsage, getMediaUsage
Form SubmissionscreateFormSubmission, listFormSubmissions, updateFormSubmissionStatus, createFormSubmissionIfAllowed
WebhookscreateWebhook, listProjectWebhooks, updateWebhook, deleteWebhook
Webhook DeliveriescreateWebhookDelivery, listWebhookDeliveries, updateWebhookDelivery
CDN KeysvalidateCDNKeyHash, createCDNKey, listCDNKeys, revokeCDNKey
CDN BuildscreateCDNBuild, updateCDNBuild, listCDNBuilds
Conversation KeysvalidateConversationKeyHash, createConversationKey, revokeConversationKey
Audit LogscreateAuditLog

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)

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:

ts
const git = useGitProvider({
  installationId: workspace.github_installation_id,
  owner: 'org',
  repo: 'my-repo',
})

GitAppProvider

Manages GitHub App installation-level operations (not repository-specific).

ts
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)

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)

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)

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)

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)

ts
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.

ts
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:

  1. Create a file in server/providers/ (e.g., clerk-auth.ts)
  2. Implement the full interface
  3. Update the factory in server/utils/providers.ts:
ts
import { createClerkAuthProvider } from '../providers/clerk-auth'

export function useAuthProvider(): AuthProvider {
  if (!_authProvider)
    _authProvider = createClerkAuthProvider()  // Swap this line
  return _authProvider
}
  1. 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.

Released under the AGPL-3.0 License.