Nuxt Studio Integration
SaaS4Builders integrates Nuxt Studio as a visual content editor for platform administrators. Instead of using Studio's built-in OAuth system, the boilerplate implements a custom Sanctum-based auth bridge — platform admins who already have the content.manage permission can open Studio directly from the manager panel, with no separate login or third-party OAuth setup required.
How It Works
The integration bridges two authentication systems:
- Laravel Sanctum (cookie-based) — Your existing admin authentication.
- Nuxt Studio sessions (H3 server sessions) — What Studio needs to authorize editing.
The bridge forwards Sanctum cookies to Laravel for verification, then creates a local Studio session. This means:
- No separate Studio accounts or OAuth configuration.
- Access is controlled by your existing permission system (
content.manage). - Revoking a platform admin's permissions immediately revokes Studio access.
Admin clicks "Content Studio"
→ Browser pre-opens new tab (avoids popup blockers)
→ POST /api/studio/login (Sanctum cookies forwarded)
→ Nitro server → GET /api/v1/admin/studio/access (Laravel)
→ auth:sanctum ✓
→ impersonation.prevent ✓
→ platform.admin ✓
→ platform.permission:content.manage ✓
→ Create H3 Studio session
→ Redirect tab to / (Studio editor loads client-side)
Studio Module Configuration
The Nuxt Studio module is configured in frontend/nuxt.config.ts:
studio: {
dev: false,
route: '/_studio',
repository: {
provider: (process.env.STUDIO_REPOSITORY_PROVIDER as 'github' | 'gitlab') || 'github',
owner: process.env.STUDIO_REPOSITORY_OWNER || '',
repo: process.env.STUDIO_REPOSITORY_NAME || '',
branch: process.env.STUDIO_REPOSITORY_BRANCH || 'main',
rootDir: 'frontend',
private: true,
},
},
| Setting | Description |
|---|---|
dev: false | Disables Studio in development mode (uses the custom bridge instead) |
route: '/_studio' | The URL path where Studio UI is served |
repository.provider | Git provider: github or gitlab |
repository.rootDir | The subdirectory containing the Nuxt app (frontend) |
repository.private | Set to true for private repositories |
Environment Variables
Add these to your deployment environment:
| Variable | Required | Description |
|---|---|---|
STUDIO_REPOSITORY_PROVIDER | Yes | github or gitlab |
STUDIO_REPOSITORY_OWNER | Yes | Repository owner (user or organization) |
STUDIO_REPOSITORY_NAME | Yes | Repository name |
STUDIO_REPOSITORY_BRANCH | No | Branch to edit (default: main) |
NUXT_PUBLIC_AUTH_MODE=cookie). Token-based authentication is not supported for the Studio bridge because Nitro server middleware cannot access Sanctum tokens — only cookies are forwarded automatically by the browser.Auth Bridge Architecture
The bridge consists of four server-side files and one client-side composable:
Server Utilities
export async function verifyStudioAccess(event: H3Event): Promise<LaravelStudioAccessUser> {
const config = useRuntimeConfig()
// V1: cookie mode only
if (config.public.authMode !== 'cookie') {
throw createError({ statusCode: 501, statusMessage: 'Studio bridge requires cookie auth mode' })
}
// Forward browser cookies to Laravel
const cookieHeader = getRequestHeader(event, 'cookie')
if (!cookieHeader) {
throw createError({ statusCode: 401, statusMessage: 'Not authenticated' })
}
// Forward Origin header so Sanctum activates session auth
const originHeader = getRequestHeader(event, 'origin') || getRequestHeader(event, 'referer')
const response = await $fetch('/api/v1/admin/studio/access', {
baseURL: config.apiBaseUrl,
method: 'GET',
headers: {
Cookie: cookieHeader,
Accept: 'application/json',
...(originHeader && { Origin: originHeader }),
},
})
return response.user
}
This utility is the core of the bridge. It extracts the browser's session cookies from the request, forwards them to Laravel's Studio access endpoint, and returns the verified user data.
Login Endpoint
export default defineEventHandler(async (event) => {
// Verify access via Laravel — throws 401/403/501/502 on failure
const user = await verifyStudioAccess(event)
// Create Studio session
await setStudioUserSession(event, {
name: user.name,
email: user.email,
avatar: user.avatar ?? undefined,
})
return {
success: true,
redirect: '/',
}
})
The login endpoint verifies the admin's access, creates a Studio H3 session with the user's name, email, and avatar, then returns a redirect URL. The redirect goes to / (not /_studio) because the Studio editor is injected client-side by the activation plugin on any page with an active session.
Logout Endpoint
export default defineEventHandler(async (event) => {
await clearStudioUserSession(event)
return { success: true }
})
No auth check is needed — destroying a non-existent session is a safe no-op. This endpoint is called by the auth store during logout to clean up the Studio session alongside the main session.
Route Protection Middleware
export default defineEventHandler(async (event) => {
const pathname = getRequestURL(event).pathname
if (!pathname.startsWith('/_studio')) return
if (pathname.startsWith('/_studio/assets')) return // Skip static assets
// No session cookie → never logged in via bridge
if (!hasStudioSession(event)) {
return sendStudio403(event)
}
// Entry point: full revalidation via Laravel
const isEntryPoint = pathname === '/_studio' || pathname === '/_studio/'
if (isEntryPoint) {
try {
await verifyStudioAccess(event)
} catch {
await clearStudioSessionSafe(event)
return sendStudio403(event)
}
}
// Sub-requests: session cookie presence check only (performance)
})
The middleware implements a two-tier protection strategy:
- Entry point (
/_studioor/_studio/): Full revalidation against Laravel on every access. This catches permission revocations — if an admin'scontent.managepermission is removed, they are blocked immediately on next Studio entry. - Sub-requests (
/_studio/*): Fast session cookie check only. These are frequent asset/API calls from the Studio UI, and full revalidation would be too slow.
Backend Access Verification
The Laravel endpoint that the bridge calls is minimal — all authorization happens in middleware:
final class StudioController extends Controller
{
public function checkAccess(Request $request): JsonResponse
{
$user = $request->user();
return response()->json([
'allowed' => true,
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'avatar' => $user->avatar
? (str_starts_with($user->avatar, 'http')
? $user->avatar
: Storage::disk('public')->url($user->avatar))
: null,
],
]);
}
}
Route: GET /api/v1/admin/studio/access
Middleware chain:
| Order | Middleware | Purpose |
|---|---|---|
| 1 | auth:sanctum | User must be authenticated |
| 2 | impersonation.prevent | Cannot access Studio while impersonating another user |
| 3 | platform.admin | User must have is_platform_admin = true |
| 4 | platform.permission:content.manage | User must have the content.manage permission |
| 5 | throttle:60,1 | Rate limited to 60 requests per minute |
If all middleware passes, the controller returns the user's profile data. The Nitro bridge uses this data to populate the Studio session.
The content.manage Permission
Access to Nuxt Studio is controlled by the content.manage permission, which is assigned to the platform-admin role through the permissions seeder. This is a binary permission — you either have access or you don't. There is no granular per-document access control.
To grant Studio access to a specific admin, ensure they have the platform-admin role which includes all platform-level permissions by default.
content.manage can edit content through Studio.Client-Side Launcher
The useStudioLauncher composable handles opening Studio from the manager panel:
export function useStudioLauncher() {
const authStore = useAuthStore()
const { t } = useI18n()
const toast = useToast()
const isOpeningStudio = ref(false)
async function openStudio(): Promise<void> {
if (!authStore.isAuthenticated || isOpeningStudio.value) return
isOpeningStudio.value = true
// Pre-open tab synchronously — must be in user gesture call stack
const studioTab = window.open('about:blank', '_blank')
try {
const response = await $fetch<{ redirect: string }>('/api/studio/login', {
method: 'POST',
credentials: 'include',
})
if (studioTab && !studioTab.closed) {
studioTab.location.href = response.redirect
} else {
// Popup was blocked — fallback to same-tab navigation
window.location.href = response.redirect
}
} catch (error) {
if (studioTab && !studioTab.closed) {
studioTab.close()
}
// Show error toast (401 → session expired, 403 → forbidden)
} finally {
isOpeningStudio.value = false
}
}
return {
isOpeningStudio: readonly(isOpeningStudio),
openStudio,
}
}
The composable pre-opens a blank tab synchronously (within the click event handler) to avoid browser popup blockers. The actual authentication and redirect happen asynchronously after the tab is open. If the bridge call fails, the pre-opened tab is closed and an error toast is displayed.
Session Management
The Studio session uses two mechanisms:
- H3 server session — Created by
setStudioUserSession()(provided by the nuxt-studio module). Stores user name, email, and avatar. studio-session-checkcookie — A lightweight cookie set alongside the session, used by the middleware for fast session presence checks without reading the full session store.
Session Lifecycle
| Event | Action |
|---|---|
| Admin clicks "Content Studio" | Bridge creates H3 session + cookie |
Admin navigates /_studio | Middleware revalidates against Laravel |
Admin navigates /_studio/edit/... | Middleware checks cookie only (fast path) |
| Admin clicks Logout (main app) | Auth store calls POST /api/studio/logout to destroy session |
| Permission revoked in Laravel | Next /_studio entry triggers revalidation → session cleared → 403 |
logout() function makes a best-effort call to POST /api/studio/logout to clean up the session. If the call fails (e.g., network error), the Studio session cookie will expire naturally.Security Considerations
Impersonation Prevention
The impersonation.prevent middleware blocks Studio access during impersonation sessions. This prevents a platform admin who is impersonating a tenant user from accidentally accessing Studio in that context, which could cause confusing session state.
No Parallel Auth System
Unlike many CMS integrations that introduce a separate OAuth flow (GitHub, Google, etc.), this bridge reuses your existing Sanctum authentication. This means:
- One less OAuth app to configure and maintain.
- No token storage or refresh logic for a third-party provider.
- Permission revocations take effect immediately (on next Studio entry).
- Session management is unified — logging out of the app logs out of Studio.
Cookie Forwarding
The bridge forwards the browser's raw cookies to Laravel via the internal Docker network. The Origin header is also forwarded to ensure Sanctum's EnsureFrontendRequestsAreStateful middleware activates session-based authentication for the forwarded request.
Workflow
The typical content editing workflow:
- Admin signs in to the platform manager panel.
- Clicks "Content Studio" in the sidebar — the
useStudioLaunchercomposable handles the bridge login. - Studio loads in a new tab with a visual editor.
- Edits content — modifies Markdown, YAML frontmatter, images, or structured fields directly in the browser.
- Saves changes — Studio commits changes to the configured Git branch.
- Deploys — Your CI/CD pipeline picks up the commit and rebuilds the site.
Content changes go through your normal Git workflow — commits, pull requests, reviews, and deployments all work as expected. Studio is a convenience layer on top of Git, not a replacement for it.
Customizing Studio Access
To restrict Studio access to specific admins (not all platform admins):
- Create a new role (e.g.,
content-editor) with only thecontent.managepermission. - Assign this role to specific users.
- The middleware chain already checks for
content.managespecifically — users with the new role will have Studio access while other admins without that permission will not.
To disable Studio entirely, remove the nuxt-studio module from frontend/nuxt.config.ts and delete the server/api/studio/ and server/middleware/studio-auth.ts files.
What's Next
- Nuxt Content Setup — Content directory structure, collections, and schemas.
- Authentication Overview — Sanctum cookie authentication that the bridge relies on.
- Settings Overview — Platform and tenant settings management.
Nuxt Content Setup
Content directory structure, collection definitions, multi-language support, Zod schemas, and MDC syntax for managing editorial content with Nuxt Content v3.
Internationalization Overview
How SaaS4Builders handles multi-language support: dual-layer translation architecture, included locales, adding or removing languages, locale detection, and what gets translated.