Skip to content

Architecture Overview ​

Contentrain Studio is a conversation-first CMS built as a single Nuxt 4 full-stack application. It is deployment-flexible: the AGPL core is self-hostable, and managed Pro/Enterprise deployments can run on the same architecture. All external services are abstracted through provider interfaces, allowing operators to swap implementations without changing application code.

Studio shares the .contentrain/ contract with the Contentrain AI package surface, but it is not an @contentrain/mcp wrapper. The package side handles local-first execution and IDE workflows; Studio adds the authenticated web layer for collaboration, review, and delivery.

System Overview ​

                         +--------------------------+
                         |      Reverse Proxy       |
                         |      (TLS termination)   |
                         +------------+-------------+
                                      |
                         +------------v-------------+
                         |   Contentrain Studio     |
                         |   Nuxt 4 / Nitro         |
                         |                          |
                         |  +-------+  +---------+  |
                         |  | App   |  | Server  |  |
                         |  | (SPA) |  | (API)   |  |
                         |  +-------+  +---------+  |
                         +------|---------|----------+
                                |         |
             +------------------+---------+------------------+
             |          |          |          |               |
       +-----v---+ +---v----+ +--v-----+ +--v----+ +--------v-------+
       | Auth    | | DB     | | Git    | | AI    | | Object Storage |
       | Provider| |Provider| |Provider| |Provider| | (CDN/Media)   |
       +---------+ +--------+ +--------+ +-------+ +----------------+
       Supabase    Supabase    GitHub     Anthropic  Cloudflare R2
       Auth        PostgreSQL  App        Claude     (ee/)
         or          or
       managed     plain
       JWT         PostgreSQL

  Auth + Database ship as a matched pair, selected at boot by
  NUXT_AUTH_PROVIDER / NUXT_DATABASE_PROVIDER:
    supabase + supabase  (default)   |   managed + postgres

Nuxt 4 Full-Stack Architecture ​

Studio is a single project -- not a monorepo. Nuxt 4 runs both the client SPA and the Nitro server in one deployment unit.

studio/
├── app/                    # Client-side (SPA, SSR disabled)
│   ├── components/         # Atomic design: atoms, molecules, organisms
│   ├── composables/        # Client-side business logic
│   ├── layouts/            # auth, default, workspace
│   ├── pages/              # File-based routing
│   └── assets/             # CSS, fonts, images
├── server/                 # Server-side (Nitro)
│   ├── api/                # REST API endpoints
│   ├── middleware/          # Auth, billing, invite acceptance, audit
│   ├── providers/          # Provider interfaces + implementations
│   └── utils/              # Agent tools, content engine, helpers
├── shared/                 # Shared code (client + server)
│   └── utils/              # License/feature flags
├── ee/                     # Enterprise Edition (proprietary)
│   ├── cdn/                # Cloudflare R2 CDN provider
│   ├── media/              # Sharp image processing
│   └── enterprise/         # Bridge + route handlers
└── .contentrain/           # Content directory (UI strings, agent prompts)

Key Configuration ​

SSR is disabled (ssr: false) because Studio is an authenticated app with no public marketing pages. The client runs as a SPA behind the auth gate.

ts
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: false,
  modules: ['@nuxt/eslint', '@nuxt/image'],
  css: ['~/assets/css/main.css'],
  // ...
})

Provider / Adapter Pattern ​

This is the most critical architectural decision in Studio. No implementation detail ever leaks into components, pages, composables, or server routes.

How It Works ​

  1. Interfaces define contracts in server/providers/:
ts
// server/providers/auth.ts
export interface AuthProvider {
  validateToken: (accessToken: string) => Promise<AuthUser | null>
  refreshSession: (refreshToken: string) => Promise<AuthTokens | null>
  getOAuthRedirectUrl: (provider: 'github' | 'google', redirectTo: string) => Promise<OAuthRedirectResult>
  exchangeCode: (code: string, state?: string) => Promise<AuthSession>
  sendMagicLink: (email: string, redirectTo: string) => Promise<void>
  deleteUser: (userId: string) => Promise<void>
  // ...
}
  1. Implementations live alongside interfaces (or in ee/). Each contract ships with more than one implementation — for example, two AuthProvider implementations:
ts
// server/providers/supabase-auth.ts
export function createSupabaseAuthProvider(): AuthProvider {
  // Uses @supabase/supabase-js internally
  // This is the ONLY file that imports Supabase client code
}

// server/providers/managed-auth.ts
export function createManagedAuthProvider(): AuthProvider {
  // Supabase-free built-in auth: signs its own HS256 JWTs,
  // runs the OAuth dance, and issues magic links via Resend
}
  1. Factories resolve singletons via server/utils/providers.ts, choosing the implementation from runtime config:
ts
// server/utils/providers.ts
export function useAuthProvider(): AuthProvider {
  if (!_authProvider) {
    const kind = useRuntimeConfig().authProvider || 'supabase'
    if (kind === 'managed') {
      ensurePostgresConfigured()
      _authProvider = createManagedAuthProvider()
    }
    else {
      _authProvider = createSupabaseAuthProvider()
    }
  }
  return _authProvider
}

The DatabaseProvider factory mirrors this, switching on NUXT_DATABASE_PROVIDER (supabase vs postgres). Auth and database are chosen as a matched pair — see the provider pair rules.

  1. Application code only imports from factories:
ts
// In any server route
const auth = useAuthProvider()
const user = await auth.validateToken(token)

Provider Inventory ​

ProviderInterfaceShipped ImplementationsSingleton?
AuthAuthProviderSupabase Auth or built-in managed JWTYes
DatabaseDatabaseProviderSupabase PostgreSQL or plain PostgreSQLYes
GitGitProviderGitHub AppNo (per-repo)
Git AppGitAppProviderGitHub App InstallationNo (per-install)
AIAIProviderAnthropic ClaudeYes
CDNCDNProviderCloudflare R2 (ee/)Yes
MediaMediaProviderSharp + R2 (ee/)Yes
EmailEmailProviderResendYes
PaymentPaymentProviderPolar (default) + Stripe (legacy)Yes
ConnectorConnectorProviderNone yetN/A

Provider pairs

Auth and Database ship as a matched pair, chosen at boot via NUXT_AUTH_PROVIDER / NUXT_DATABASE_PROVIDER. Two pairs are supported: supabase + supabase (default) and managed + postgres (Supabase-free, plain PostgreSQL 14+). Mixed pairs are rejected at boot. The managed auth implementation lives in server/providers/managed-auth.ts and the plain-PostgreSQL implementation in server/providers/postgres-db/.

Rules -- Never Violate

  • NEVER import @supabase/supabase-js outside of server/providers/
  • NEVER use useSupabaseClient() or useSupabaseUser() in components
  • ALL auth checks use AuthProvider.getSession(), never Supabase directly
  • Provider instances are resolved via factory functions only

Workspace Hierarchy ​

Studio organizes data in a three-level hierarchy:

User
  └── Workspace (billing entity)
        ├── Members (owner, admin, member)
        ├── GitHub App Installation
        └── Projects (connected repositories)
              ├── Members (editor, reviewer, viewer)
              ├── Conversations
              ├── Content (via Git)
              └── Media Assets

Key Rules ​

  • Signup auto-creates a primary workspace (type: primary)
  • Each workspace has exactly one owner (the GitHub-authenticated user)
  • GitHub App installation lives on the workspace, not the project
  • Workspace Owner/Admin has implicit access to all projects
  • Workspace Member needs explicit project_members assignment

Component Architecture ​

Studio follows atomic design principles, adapted for Radix Vue + Tailwind CSS 4:

app/components/
├── atoms/          # Radix Vue primitives + Tailwind
│   ├── HeadingText.vue
│   ├── BaseButton.vue
│   ├── FormInput.vue
│   ├── FormLabel.vue
│   ├── Badge.vue
│   └── Avatar.vue
├── molecules/      # Composed atoms
│   ├── ProviderButtons.vue
│   ├── AuthLink.vue
│   └── EmailButton.vue
└── organisms/      # Business logic components
    ├── SigninWithProvider.vue
    ├── ProfileOverviewPanel.vue
    └── WorkspaceMembersPanel.vue

There is no templates/ layer -- Nuxt layouts (app/layouts/) handle page-level wrappers.

Layout System ​

LayoutPurposeStructure
authLogin/callback pagesSplit panel (form left, marketing right)
defaultWorkspace list, settingsSidebar (240px) + main content
workspaceProject workspaceThree-panel (sidebar 240px, chat, context 400px)

Data Flow ​

Authentication Flow ​

Client                    Server                     Supabase
  |                         |                           |
  |-- POST /api/auth/login -|                           |
  |                         |-- getOAuthRedirectUrl() --|
  |<- { url } -------------|                           |
  |                         |                           |
  |-- (OAuth redirect) -----|-------------------------->|
  |<- (callback with code) -|                           |
  |                         |                           |
  |-- POST /api/auth/verify |                           |
  |                         |-- exchangeCode() -------->|
  |                         |<- session + tokens -------|
  |                         |-- setServerSession() -----|
  |<- { user } ------------|                           |

Content Operation Flow ​

User Chat Message
     |
     v
Intent Classification
     |
     v
System Prompt Construction
(config, models, permissions, UI context, vocabulary)
     |
     v
AI Provider (streaming)
     |
     +--> Tool Call (save_content, save_model, etc.)
     |       |
     |       v
     |    State Machine Guard
     |       |
     |       v
     |    Content Engine
     |       |
     |       v
     |    GitProvider (branch -> commit -> merge)
     |       |
     |       v
     |    Brain Cache Invalidation
     |
     v
SSE Events -> Client UI

Key Design Decisions ​

Session Management ​

Sessions use AES-256 encrypted httpOnly cookies managed by h3 useSession(). The auth middleware automatically refreshes tokens 5 minutes before expiry.

Git-Based Content ​

All content is stored in Git repositories using the .contentrain/ directory format. Studio never stores content in its database -- Git is the single source of truth. The contentrain branch serves as the SSOT branch, with cr/* feature branches for individual operations.

Enterprise Bridge ​

Enterprise features (ee/) are loaded at runtime via a dynamic import bridge pattern. If ee/ is absent, the bridge returns null and features degrade gracefully (returning 403 for EE-only routes).

Feature Flags ​

All feature gating uses hasFeature(plan, 'feature.name'). Plan checks are never hardcoded. The FEATURE_MATRIX in shared/utils/license.ts is the runtime helper, but it is derived at build time from .contentrain/content/system/plan-features/data.json -- that content file is the single source of truth for which plans grant each feature (plus the requires_ee and roadmap flags).

Released under the AGPL-3.0 License.