Open-core product for team content management over Git. This repository contains the AGPL-3.0 core. Conversation-first CMS — chat with AI agent to manage structured content.
- Framework: Nuxt 4 (full-stack, single project — NOT monorepo)
- UI: Radix Vue (headless primitives) + Tailwind CSS 4 (CSS-based config, @theme)
- Auth: AuthProvider interface — current impl: Supabase Auth (GitHub OAuth, Google OAuth, Magic Link)
- Database: DatabaseProvider interface — current impl: Supabase PostgreSQL with RLS
- Content: Contentrain MCP + @contentrain/query SDK for UI strings
- Icons: Annon custom icon set via @iconify/tailwind4
- Images: @nuxt/image (NuxtImg for all images)
Studio is deployment-flexible. The AGPL core is self-hostable, and the same product model can also be operated as a managed Pro/Enterprise service. All external services are accessed only through provider interfaces. No implementation detail ever leaks into components, pages, composables, or server routes.
Provider interfaces live in server/providers/:
server/providers/auth.ts— AuthProvider interfaceserver/providers/database.ts— DatabaseProvider interfaceserver/providers/storage.ts— StorageProvider interfaceserver/providers/git.ts— GitProvider interface
Concrete implementations live alongside interfaces in server/providers/, named [impl]-[service].ts:
server/providers/supabase-auth.ts— SupabaseAuthProviderserver/providers/supabase-db.ts— SupabaseDatabaseProvider (future)
Rules — never violate:
- NEVER import
@supabase/supabase-jsoutside ofserver/providers/impl/ - NEVER use
useSupabaseClient(),useSupabaseUser(),useSupabaseSession()in components or pages - NEVER use
@nuxtjs/supabaseauto-injected composables outside the adapter layer - NEVER write Supabase-specific SQL (RLS policies,
auth.usersjoins) in application logic — only in migration files - ALL auth checks in server routes use
AuthProvider.getSession(), never the Supabase client directly - Provider instances are resolved via a factory/injection in
server/utils/providers.ts
Current implementation: Supabase (Auth + PostgreSQL). Alternative implementations (future): standard OAuth + plain PostgreSQL, Auth0, Clerk, etc. Any new provider implementation must satisfy the same interface — zero application code changes required.
- Owner: GitHub OAuth (needs repo access)
- Invited users: Google OAuth or Magic Link (no password)
- Session: h3 useSession() AES-256 encrypted httpOnly cookie, auto-refresh
- Provider factory:
useAuthProvider()singleton viaserver/utils/providers.ts - Two-tier roles:
- Workspace: Owner, Admin, Member
- Project: Editor, Reviewer, Viewer
- Model-specific access:
specificModels+allowedModelson project_members - Chat agent security: permissions in system prompt + API-level enforcement
User → Workspace (billing entity) → Project (connected repo)
- Signup auto-creates personal workspace (type: primary)
- Workspace Owner/Admin → implicit access to all projects
- Workspace Member → needs explicit project_members assignment
- GitHub App installation lives on workspace (not project)
This repository is the AGPL core product surface — no marketing pages. All routes are authenticated:
/auth/login ............... Auth (public)
/auth/callback ............ Auth callback (public)
/ ......................... Workspace list (or redirect to default)
/w/:slug .................. Workspace dashboard — project list
/w/:slug/projects/new ..... Connect repository
/w/:slug/projects/:id ..... Project workspace (three-panel)
/w/:slug/settings ......... Workspace settings, members, billing
/settings ................. User/account settings
Exact match of old Contentrain CMS tailwind-preset. NEVER violate these rules:
- NO
gray-*— usesecondary-*(= slate) - NO
red-*— usedanger-* - NO
green-*— usesuccess-* - NO
yellow-*/amber-*— usewarning-* - NO
blue-*— useprimary-* - NO
purple-*/indigo-*— useinfo-*
primary(brand blue 50-900)secondary(slate/neutral 50-950) — ALL neutral UIsuccess(green 50-900)warning(amber 50-900)danger(red 50-900)info(purple 50-900)
text-heading— headings, strong text (secondary-900)text-body— body paragraphs (secondary-600)text-muted— helper/secondary text (secondary-400)text-label— form labels (secondary-500)text-disabled— disabled state (secondary-300)border-border— dividers, borders (secondary-200)bg-error— error backgrounds (danger-100)
- Page:
bg-white dark:bg-secondary-950 - Raised/card:
bg-secondary-50 dark:bg-secondary-900 - Input:
bg-white dark:bg-secondary-900
- Default:
border-secondary-200 dark:border-secondary-800
- Heading:
text-secondary-900 dark:text-secondary-100 - Body:
text-secondary-600 dark:text-secondary-300
text-muted,text-label— these are mid-tone and readable on both light/dark
All user-facing text comes from Contentrain dictionary via @contentrain/query SDK:
const { t } = useContent()
t('auth.sign_in_title') // => "Sign in to your account"Dictionary model: ui-strings in .contentrain/content/system/ui-strings/en.json
When adding new UI text:
- Add the key/value to the dictionary via
contentrain_content_saveMCP tool - Run
npx contentrain generateto update SDK client - Use
useContent().t('key')in the component
Atomic design from old Contentrain CMS, refactored for Radix Vue + Tailwind 4:
app/components/
atoms/ — Radix Vue primitives + Tailwind (HeadingText, BaseButton, FormInput, FormLabel, Logo, Avatar, Badge)
molecules/ — Composed atoms (ProviderButtons, AuthLink, EmailButton)
organisms/ — Business logic components (SigninWithProvider, SigninWithEmail)
Note: No
templates/layer — Nuxt layouts (app/layouts/) handle page-level wrappers.
- All atoms use Radix Vue primitives where applicable
- All components support dark mode
- NuxtImg for all images (not
)
- Icons via
icon-[annon--home-2]class (not inline SVG, except brand logos)
W3C + WCAG compliance for all interactive elements. NEVER violate these rules:
Cursor:
- Global
cursor: pointeris set inmain.css@layer baseforbutton,[role="button"],a[href],summary,select— do NOT addcursor-pointerclass to these elements individually disabled:cursor-not-allowedon disabled interactive elements (already handled in atoms)
Button type attribute:
- ALL
<button>elements MUST have an explicittypeattribute - Default:
type="button"— prevents accidental form submission - Only use
type="submit"inside<form>elements for the primary submit action - BaseButton atom has
typeprop (default:"button") andvariantprop (ghost/primary/danger/secondary)
Focus visibility (keyboard navigation):
- ALL interactive elements MUST have
focus-visiblestyles for keyboard accessibility - Standard pattern:
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 - Add
roundedto inline links/buttons so the focus ring follows the shape - Inputs use
focus:ring(notfocus-visible) — mouse focus indication is correct for form fields
ARIA attributes:
- Inputs with error state:
aria-invalid="true" - Inputs with description text:
aria-describedbylinking to description element - Decorative icons (required asterisk, etc.):
aria-hidden="true" - NEVER use raw
<button>without accessibility attributes in organisms — prefer atom components
Annon icon set (800+ custom icons) via @iconify/tailwind4:
<span class="icon-[annon--home-2] text-muted" />
<span class="icon-[annon--alert-triangle] text-danger-500" />Brand SVGs (GitHub, Google logos) stay as inline SVG — they need exact brand colors.
auth— split panel (form left, marketing right)default— sidebar-first (AppSidebar 240px + full-width main). No header.workspace— three-panel (sidebar 240px, chat, context 400px)
Deferred roadmap and planning notes live outside the tracked repository. Key items:
- GitHub webhook for installation lifecycle (Phase 2)
- GitProvider write operations — stubs now, Phase 2
- ⌘K Command palette (Phase 2)
- Team workspace + personal GitHub account warning (Phase 4)
- Workspace ownership transfer before deletion (Phase 4)
- Admin RLS for workspace_members management (Phase 4)
- Sidebar mobile responsive (hamburger + slide-over)
- Supabase JWT decode for accurate token expiry
- Conventional Commits enforced by commitlint + husky
pnpm lint/pnpm lint:fix— @nuxt/eslint with Stylistic (no Prettier)pnpm release— full local release gate (release:check) + changelog/version/tag flow- lint-staged on pre-commit (only changed files)
- GitHub Actions CI on PRs (commit lint + build)
Studio uses Open Core model: AGPL core + proprietary ee/ directory.
The ee/ boundary rules are enforced in code and contributor guidance; internal planning files are not tracked.
- ee/ directory has its own proprietary LICENSE — NEVER mix ee/ code into core
- Core must work without ee/ — starter tier (AGPL core) is a fully functional product
- Feature flags via
server/utils/license.ts→hasFeature(plan, 'feature.name') - NEVER hardcode plan checks — always use
hasFeature()function - Provider interfaces in core, implementations can be in ee/ (same pattern as AuthProvider)
- UI conditional rendering based on plan — use
hasFeature()in computed properties - Database schema stays in core — ee/ columns exist but are unused/RLS-gated in starter tier
- Graceful degradation — if ee/ feature is unavailable, degrade safely (reviewer → editor, not error)
- Advanced roles (reviewer, viewer, specificModels)
- BYOA API key management
- Premium connectors (Canva, Figma, Recraft, Notion, Google Drive)
- SSO (SAML, OIDC)
- Approval chains, scheduled publish
- Audit log, activity feed
- White-label branding
- Outbound webhooks
- All auth flows, workspace/project CRUD
- Chat engine + all agent tools
- Content CRUD (all 4 kinds, all 27 field types)
- Auto-merge workflow, branch creation
- Content editor modal, all UI components
- Owner + Editor roles
- URL fetch connector
- Single + multi-locale (config-driven, not plan-gated)
Long-form planning specs and phase notes are intentionally kept out of the tracked repository.
.tmp/contentrain/ contains the old Contentrain headless CMS.
Used as architectural reference for UI patterns, role/permission system, and theme.
NOT part of Studio code — gitignored.
The old CMS UI/UX is approved and is the visual/UX basis for Studio components. You can and should port components/views from the old CMS — but take only the UI/UX, not the code.
What to take from the old CMS:
- Visual layout and spacing
- Component composition and flow (what goes where, in what order)
- User interaction patterns (form flows, button placement, navigation)
What NOT to carry over:
- Code structure, file naming, directory organization
- Old component/variable naming conventions
- Code quality issues, anti-patterns, TODOs, hacks
- Pinia store logic, Vue Router usage, Firebase/LuiButton imports
- Hardcoded strings
How to port:
- Rewrite from scratch using the old CMS as a visual reference
- Follow Studio's atomic structure: atoms → molecules → organisms
- Apply
skills-lock.json(antfu) standards — linting, naming, code style enforced automatically - Replace all tech references: Vue Router → NuxtLink, LuiButton → Radix atoms, Firebase → AuthProvider, hardcoded strings →
t('key'), raw Tailwind colors → Studio palette,<img>→<NuxtImg>
Key auth paths to reference:
.tmp/contentrain/client/src/
layouts/auth.vue → app/layouts/auth.vue
components/organisms/SigninWithProvider.vue → app/components/organisms/SigninWithProvider.vue
components/organisms/SigninWithEmail.vue → app/components/organisms/SigninWithEmail.vue
pages/auth/signin.vue → app/pages/auth/login.vue
atoms/ molecules/ → app/components/atoms/ molecules/