Skip to content
SaaS4Builders
Content Management

Nuxt Studio Integration

Visual content editing with Nuxt Studio, secured by a custom Sanctum-based auth bridge that reuses your existing platform admin permissions.

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:

  1. Laravel Sanctum (cookie-based) — Your existing admin authentication.
  2. 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:

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,
  },
},
SettingDescription
dev: falseDisables Studio in development mode (uses the custom bridge instead)
route: '/_studio'The URL path where Studio UI is served
repository.providerGit provider: github or gitlab
repository.rootDirThe subdirectory containing the Nuxt app (frontend)
repository.privateSet to true for private repositories

Environment Variables

Add these to your deployment environment:

VariableRequiredDescription
STUDIO_REPOSITORY_PROVIDERYesgithub or gitlab
STUDIO_REPOSITORY_OWNERYesRepository owner (user or organization)
STUDIO_REPOSITORY_NAMEYesRepository name
STUDIO_REPOSITORY_BRANCHNoBranch to edit (default: main)
The auth mode must be set to cookie (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

frontend/server/utils/studio.ts
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

frontend/server/api/studio/login.post.ts
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

frontend/server/api/studio/logout.post.ts
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

frontend/server/middleware/studio-auth.ts
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 (/_studio or /_studio/): Full revalidation against Laravel on every access. This catches permission revocations — if an admin's content.manage permission 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:

backend/app/Http/Controllers/Api/V1/Admin/StudioController.php
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:

OrderMiddlewarePurpose
1auth:sanctumUser must be authenticated
2impersonation.preventCannot access Studio while impersonating another user
3platform.adminUser must have is_platform_admin = true
4platform.permission:content.manageUser must have the content.manage permission
5throttle:60,1Rate 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 is global — it is not scoped to any tenant. Documentation, blog posts, pricing pages, and changelogs are shared across the entire platform. Only platform administrators with content.manage can edit content through Studio.

Client-Side Launcher

The useStudioLauncher composable handles opening Studio from the manager panel:

frontend/app/composables/useStudioLauncher.ts
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-check cookie — A lightweight cookie set alongside the session, used by the middleware for fast session presence checks without reading the full session store.

Session Lifecycle

EventAction
Admin clicks "Content Studio"Bridge creates H3 session + cookie
Admin navigates /_studioMiddleware 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 LaravelNext /_studio entry triggers revalidation → session cleared → 403
Studio sessions are destroyed on logout from the main application. The auth store's 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.

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:

  1. Admin signs in to the platform manager panel.
  2. Clicks "Content Studio" in the sidebar — the useStudioLauncher composable handles the bridge login.
  3. Studio loads in a new tab with a visual editor.
  4. Edits content — modifies Markdown, YAML frontmatter, images, or structured fields directly in the browser.
  5. Saves changes — Studio commits changes to the configured Git branch.
  6. 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):

  1. Create a new role (e.g., content-editor) with only the content.manage permission.
  2. Assign this role to specific users.
  3. The middleware chain already checks for content.manage specifically — 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