Features

SuperSaaS Integration

SuperSaaS integration layer for Nuxt Crouton - connectors, translations, and utilities
Status: Stable - @friendlyinternet/nuxt-crouton-supersaas provides production-ready SuperSaaS integration.

The @friendlyinternet/nuxt-crouton-supersaas package provides pre-built connectors for integrating SuperSaaS auth systems with Nuxt Crouton, app-level i18n locale files, and utilities for team-based applications.

Package Overview

Package: @friendlyinternet/nuxt-crouton-supersaasVersion: 1.0.0 Type: SuperSaaS Integration Layer

What It Does

This package serves two main purposes:

  1. External Collection Connectors - When using Crouton's CroutonReferenceSelect with external collections (like users from your auth system), this package provides ready-to-use implementations so you can configure instead of writing from scratch.
  2. App-Level i18n Locale Files - Ships with pre-defined translation strings for common app functionality (auth, navigation, teams, errors) that merge with your layer translations.

Installation

pnpm add @friendlyinternet/nuxt-crouton-supersaas

i18n Locale Files

The package includes app-level translation strings that serve as defaults for common UI elements.

Included Translations

// Example: en.json
{
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete",
    "edit": "Edit",
    "create": "Create",
    "loading": "Loading...",
    "noResults": "No results found",
    "confirmDelete": "Are you sure you want to delete this?"
  },
  "auth": {
    "signIn": "Sign In",
    "signOut": "Sign Out",
    "signUp": "Sign Up",
    "forgotPassword": "Forgot Password?",
    "resetPassword": "Reset Password",
    "email": "Email",
    "password": "Password"
  },
  "navigation": {
    "dashboard": "Dashboard",
    "settings": "Settings",
    "profile": "Profile",
    "teams": "Teams",
    "admin": "Admin"
  },
  "teams": {
    "members": "Members",
    "invite": "Invite Member",
    "role": "Role",
    "owner": "Owner",
    "admin": "Admin",
    "member": "Member"
  },
  "errors": {
    "generic": "Something went wrong",
    "notFound": "Not found",
    "unauthorized": "Unauthorized",
    "forbidden": "Forbidden"
  }
}

Supported Locales

  • English (en)
  • Dutch (nl)
  • French (fr)

Merge Order

Translations merge in this order (last wins):

1. nuxt-crouton/i18n/locales/        (Crouton component strings)
2. nuxt-crouton-supersaas/i18n/locales/ (App strings - this package)
3. layers/[domain]/i18n/locales/      (Domain-specific strings)
4. app/i18n/locales/                   (App-level overrides)

SuperSaaS Connector

The primary connector for team-based user management systems.

Usage Patterns

The connector package supports two usage patterns depending on your needs.

Best for: Most use cases - connects to existing endpoints without file copying.

Proxy mode forwards requests to your existing API endpoints and transforms responses to Crouton format. No duplicate endpoints, single source of truth.

Benefits:

  • No file copying or maintenance
  • No duplicate endpoints
  • Single source of truth (your existing endpoints)
  • Easy to update and customize

Configuration:

// app.config.ts
import { connectSupersaas } from '@friendlyinternet/nuxt-crouton-supersaas/supersaas'
import { z } from 'zod'

const userSchema = z.object({
  id: z.string(),
  title: z.string(),
  email: z.string().optional(),
  avatarUrl: z.string().optional(),
  role: z.string().optional()
})

export default defineAppConfig({
  croutonCollections: {
    users: connectSupersaas({
      sourceEndpoint: 'members', // Uses your existing /api/teams/[id]/members
      schema: userSchema,
      transform: (member) => ({
        id: member.userId,
        title: member.name,
        email: member.email,
        avatarUrl: member.avatarUrl,
        role: member.role
      })
    })
  }
})

Pattern 2: Copy-Paste Mode

Best for: Heavy customization - when you need full control over endpoint logic.

Copy connector files into your project for maximum flexibility and customization.

# Install package
pnpm add @friendlyinternet/nuxt-crouton-supersaas

# Copy connector files to your project
cp -r node_modules/@friendlyinternet/nuxt-crouton-supersaas/connectors/supersaas/app/composables/useUsers.ts ./app/composables/
cp -r node_modules/@friendlyinternet/nuxt-crouton-supersaas/connectors/supersaas/server/* ./server/

Then register in app.config.ts:

import { usersConfig } from './composables/useUsers'

export default defineAppConfig({
  croutonCollections: {
    users: usersConfig
  }
})

Requirements

Your SuperSaaS project must have:

Required for All Operations:

  1. Team-based routing: /api/teams/[id]/*
  2. validateTeamOwnership() function for auth:
    // Expected in: server/utils/teamValidation.ts (or similar)
    export async function validateTeamOwnership(event: H3Event, teamId: string) {
      // Throws error if user doesn't have access
    }
    

Required for Read Operations (List, View): 3. getActiveTeamMembers() function that returns team members:

// Expected in: server/database/queries/teams.ts (or similar)
export async function getActiveTeamMembers(teamId: string) {
  // Returns array of team members
}

Optional for Update/Delete Operations: 4. updateTeamMember() - For member updates 5. deleteTeamMember() - For member deletion

Note: Most projects only need read operations (to select users in forms). Update/Delete operations are only needed if you want to manage users through Crouton.

Quick Setup (Proxy Mode)

Step 1: Install Package

pnpm add @friendlyinternet/nuxt-crouton-supersaas

Step 2: Configure in app.config.ts

import { connectSupersaas } from '@friendlyinternet/nuxt-crouton-supersaas/supersaas'
import { z } from 'zod'

const userSchema = z.object({
  id: z.string(),
  title: z.string(),
  email: z.string().optional(),
  avatarUrl: z.string().optional(),
  role: z.string().optional()
})

export default defineAppConfig({
  croutonCollections: {
    users: connectSupersaas({
      sourceEndpoint: 'members', // Your existing /api/teams/[id]/members endpoint
      schema: userSchema,
      transform: (member) => ({
        id: member.userId,
        title: member.name,
        email: member.email,
        avatarUrl: member.avatarUrl,
        role: member.role
      })
    })
  }
})

Done!

Your forms with collection="users" will now work:

<CroutonReferenceSelect collection="users" v-model="state.updatedBy" />

How it works: The connector proxies requests to your existing SuperSaaS /api/teams/[id]/members endpoint and transforms the response to Crouton format.

What Gets Created (Copy-Paste Mode)

After copying, you'll have these files:

your-project/
├── app/
│   └── composables/
│       └── useUsers.ts                  # Collection config
└── server/
    └── api/
        └── teams/
            └── [id]/
                └── members/
                    ├── index.get.ts             # List all members (required)
                    ├── [memberId].get.ts        # Get single member (required)
                    ├── [memberId].patch.ts      # Update member (optional)
                    └── [memberId].delete.ts     # Delete member (optional)

Customization Examples

Add Custom Fields

Edit app/composables/useUsers.ts to extend the schema with custom fields:

const userSchema = z.object({
  // ... base fields (id, title, email, avatarUrl, role)
  // Add your custom fields:
  department: z.string().optional(),
  phoneNumber: z.string().optional()
})

Customize Display Format

Edit the transform function to customize how users appear in dropdowns:

transform: (member) => ({
  id: member.userId,
  title: `${member.name} (${member.role})`, // Show role in dropdown
  email: member.email,
  avatarUrl: member.avatarUrl,
  role: member.role,
  department: member.department
})

Filter Users

Edit the fetch function in server/api/teams/[id]/users/index.get.ts:

async (event) => {
  const teamId = getRouterParam(event, 'id')
  await validateTeamOwnership(event, teamId!)

  const members = await getActiveTeamMembers(teamId!)

  // Filter to only show admins and owners:
  return members.filter(m => ['admin', 'owner'].includes(m.role))
}

Users shouldn't manually select themselves. Auto-populate in your API handlers:

// Example: server/api/teams/[id]/bookings-bookings/[itemId].patch.ts
export default defineEventHandler(async (event) => {
  const session = await getUserSession(event)
  const body = await readBody(event)

  // Auto-populate with current user
  body.updatedBy = session.user.id

  // ... rest of your update logic
})

Then remove the updatedBy field from your forms since users don't need to see it.

Helper Functions

The package provides two core helper functions for creating connectors.

defineExternalCollection

Creates a collection configuration for external resources.

Signature:

function defineExternalCollection(config: ExternalCollectionConfig): CollectionConfig

Parameters:

interface ExternalCollectionConfig {
  name: string              // Collection name (must match app.config.ts key)
  schema: z.ZodSchema       // Zod schema for validation
  apiPath?: string          // API path (defaults to collection name)
  fetchStrategy?: 'query' | 'restful'  // Default: 'query'
  readonly?: boolean        // Default: true (read-only)
  meta?: {
    label?: string
    description?: string
  }
  proxy?: {
    enabled: boolean
    sourceEndpoint: string
    transform: (item: any) => { id: string; title: string; [key: string]: any }
  }
}

Example:

import { defineExternalCollection } from '@friendlyinternet/nuxt-crouton/app/composables/useExternalCollection'
import { z } from 'zod'

const userSchema = z.object({
  id: z.string(),
  title: z.string(), // Required for CroutonReferenceSelect display
  email: z.string().optional(),
  avatarUrl: z.string().optional()
})

export const usersConfig = defineExternalCollection({
  name: 'users',
  schema: userSchema,
  readonly: true,
  meta: {
    label: 'Users',
    description: 'Team members from auth system'
  }
})

createExternalCollectionHandler

Creates API handlers for external collections with automatic transformation.

Signature:

function createExternalCollectionHandler<T>(
  fetchFn: (event: H3Event) => Promise<T[]> | T[],
  transform: (item: T) => { id: string; title: string; [key: string]: any }
): EventHandler

Example:

// server/api/teams/[id]/users/index.get.ts
import { createExternalCollectionHandler } from '@friendlyinternet/nuxt-crouton'

export default createExternalCollectionHandler(
  async (event) => {
    const teamId = getRouterParam(event, 'id')
    await validateTeamOwnership(event, teamId!)
    return await getActiveTeamMembers(teamId!)
  },
  (member) => ({
    id: member.userId,
    title: member.name,        // Required for dropdown display
    email: member.email,
    avatarUrl: member.avatarUrl,
    role: member.role
  })
)

Features:

  • Automatic error handling
  • Support for ?ids= query parameter (for single item fetches)
  • Consistent response format
  • Type-safe transformations

Data Flow

Understanding how connector data flows through your application:

1. User opens form
     ↓
2. <CroutonReferenceSelect collection="users" />
     ↓
3. Looks up "users" in app.config.ts → Found!
     ↓
4. GET /api/teams/{teamId}/users
     ↓
5. validateTeamOwnership() checks access
     ↓
6. getActiveTeamMembers() fetches members
     ↓
7. Transform to Crouton format: { id, title, ... }
     ↓
8. Dropdown shows: "Alice", "Bob", etc.
     ↓
9. User selects "Alice"
     ↓
10. Form saves: updatedBy: "alice-user-id"

Troubleshooting

Error: "Collection 'users' not registered"

Make sure you registered the collection in app.config.ts:

import { usersConfig } from './composables/useUsers'

export default defineAppConfig({
  croutonCollections: {
    users: usersConfig  // Must be present
  }
})

Error: "getActiveTeamMembers is not defined"

The API endpoint expects this function. Update the import to match your project structure:

// Change this line to match your project:
// import { getActiveTeamMembers } from '@@/server/database/queries/teams'
import { getActiveTeamMembers } from '~/path/to/your/function'

Error: "validateTeamOwnership is not defined"

Update the import to match your project:

// import { validateTeamOwnership } from '@@/server/utils/teamValidation'
import { validateTeamOwnership } from '~/path/to/your/function'

Users Dropdown is Empty

Check your API endpoint is returning data:

# Test the endpoint
curl http://localhost:3000/api/teams/YOUR_TEAM_ID/users

Should return:

[
  { "id": "user1", "title": "Alice", "email": "alice@example.com" },
  { "id": "user2", "title": "Bob", "email": "bob@example.com" }
]

If empty, verify:

  • getActiveTeamMembers() is returning data
  • validateTeamOwnership() is not blocking access
  • Transform function is correct

Creating Custom Connectors

You can create connectors for other auth systems by following the SuperSaaS pattern.

Connector Structure

connectors/
└── your-system/
    ├── README.md                           # Usage documentation
    ├── index.ts                            # Export + factory function
    ├── app/
    │   └── composables/
    │       └── useUsers.ts                 # Collection config
    └── server/
        └── api/
            └── [your-routes]/
                └── users/
                    ├── index.get.ts        # List endpoint
                    └── [userId].get.ts     # Single item endpoint

Implementation Checklist

  1. Collection Config (useUsers.ts):
    • Define Zod schema with required id and title fields
    • Use defineExternalCollection() helper
    • Set readonly: true if users shouldn't edit via Crouton
  2. API Endpoints:
    • Implement list endpoint (required)
    • Implement single item endpoint (required)
    • Use createExternalCollectionHandler() for consistent responses
    • Add proper error handling
  3. Transform Function:
    • Convert external format to { id, title, ...other } format
    • Ensure title is user-friendly (shown in dropdowns)
    • Include all fields needed by forms
  4. Factory Function (for proxy mode):
    • Accept configuration object
    • Return defineExternalCollection() result
    • Include transform logic
  5. Documentation:
    • Requirements section (what functions/endpoints are needed)
    • Quick setup guide
    • Customization examples
    • Troubleshooting section

Architecture

This package follows the same addon pattern as other Crouton addons:

@friendlyinternet/nuxt-crouton          ← Core (provides utilities)
@friendlyinternet/nuxt-crouton-assets   ← Asset management addon
@friendlyinternet/nuxt-crouton-i18n     ← i18n addon
@friendlyinternet/nuxt-crouton-supersaas ← SuperSaaS integration (this package)

Design Principles

  1. Single Source of Truth: Connectors proxy to existing endpoints, not duplicate them
  2. Minimal Configuration: Simple config in app.config.ts, no complex setup
  3. Type-Safe: Full TypeScript support with Zod schemas
  4. Framework Agnostic: Works with any auth system that has HTTP endpoints
  5. Composable: Mix and match with other Crouton features

Benefits

  • No Duplicate User Management - External system remains source of truth
  • Team-Scoped - Users only see their team members (SuperSaaS)
  • Secure - Reuses existing auth validation
  • Type-Safe - Full TypeScript support with Zod
  • Customizable - Easy to modify for your needs
  • No Lock-In - Copy-paste mode for full control
  • i18n Ready - Includes app-level translations out of the box

API Reference

connectSupersaas()

Factory function for SuperSaaS proxy mode.

function connectSupersaas(config: SupersaasConnectorConfig): ExternalCollectionConfig

Parameters:

interface SupersaasConnectorConfig {
  sourceEndpoint: string    // e.g., 'members' for /api/teams/[id]/members
  schema: z.ZodSchema      // Zod validation schema
  transform: (item: any) => { id: string; title: string; [key: string]: any }
  meta?: {
    label?: string
    description?: string
  }
}

useUsers / useTeamMembers

Pre-configured collection configs for SuperSaaS (copy-paste mode).

import { usersConfig, useUsers } from '@friendlyinternet/nuxt-crouton-supersaas/supersaas'
import { teamMembersConfig, useTeamMembers } from '@friendlyinternet/nuxt-crouton-supersaas/supersaas'

// usersConfig: Minimal user reference (id, title, email)
// teamMembersConfig: Full team member data (includes role, teamId)

Need Help?

  • GitHub Issues: nuxt-crouton/issues
  • Connector Requests: Use "Enhancement" label for new connectors
  • Bug Reports: Use "SuperSaaS" label
  • Discussions: Share your integration patterns and use cases