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

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/):
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
}
  1. Factories resolve singletons via server/utils/providers.ts:
ts
// server/utils/providers.ts
export function useAuthProvider(): AuthProvider {
  if (!_authProvider)
    _authProvider = createSupabaseAuthProvider()
  return _authProvider
}
  1. Application code only imports from factories:
ts
// In any server route
const auth = useAuthProvider()
const user = await auth.validateToken(token)

Provider Inventory

ProviderInterfaceCurrent ImplementationSingleton?
AuthAuthProviderSupabase AuthYes
DatabaseDatabaseProviderSupabase PostgreSQLYes
GitGitProviderGitHub AppNo (per-repo)
Git AppGitAppProviderGitHub App InstallationNo (per-install)
AIAIProviderAnthropic ClaudeYes
CDNCDNProviderCloudflare R2 (ee/)Yes
MediaMediaProviderSharp + R2 (ee/)Yes
EmailEmailProviderResendYes
PaymentPaymentProviderStripeYes
ConnectorConnectorProviderNone yetN/A

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 single source of truth.

Released under the AGPL-3.0 License.