Overview
Workspaces provide complete organizational isolation in AI Studio. Each workspace has its own projects, users, billing, and settings. Think of workspaces as separate accounts for different companies or teams.
Workspace Architecture
Data Model
// Source: lib/db/schema.ts
export const workspace = pgTable ( "workspace" , {
id: text ( "id" ). primaryKey (),
name: text ( "name" ). notNull (),
slug: text ( "slug" ). notNull (). unique (),
// Company details
organizationNumber: text ( "organization_number" ),
contactEmail: text ( "contact_email" ),
contactPerson: text ( "contact_person" ),
// White-label branding
logo: text ( "logo" ),
primaryColor: text ( "primary_color" ),
secondaryColor: text ( "secondary_color" ),
// Status & billing
status: text ( "status" ). notNull (). default ( "active" ),
plan: text ( "plan" ). notNull (). default ( "free" ),
// Onboarding
onboardingCompleted: boolean ( "onboarding_completed" ). default ( false ),
createdAt: timestamp ( "created_at" ). notNull (). defaultNow (),
});
Workspace States
Active Full access to all features. Users can:
Create projects
Generate content
Invite team members
Access billing
Trial Limited trial period. Restrictions:
Trial expiration date
Feature limitations
Upgrade prompts
Suspended Access restricted. Reasons:
Payment failure
Policy violation
Manual suspension
suspendedAt : timestamp ( "suspended_at" ),
suspendedReason : text ( "suspended_reason" ),
Creating a Workspace
New workspaces are created during user onboarding:
Sign Up
User creates an account via email or OAuth. // Source: lib/db/schema.ts
export const user = pgTable ( "user" , {
id: text ( "id" ). primaryKey (),
email: text ( "email" ). notNull (). unique (),
workspaceId: text ( "workspace_id" ). references (() => workspace . id ),
role: text ( "role" ). notNull (). default ( "member" ),
});
Company Information
Collect organization details: Required Fields
Optional Fields
Company name
Contact email
Contact person name
Organization number (for invoicing)
Company logo
Brand colors
organizationNumber : text ( "organization_number" ),
contactEmail : text ( "contact_email" ),
contactPerson : text ( "contact_person" ),
Generate Slug
Create a unique URL-friendly identifier: // Example: "acme-real-estate" from "Acme Real Estate"
slug : text ( "slug" ). notNull (). unique ()
Slugs are used in:
Workspace URLs: /workspace/acme-real-estate
API endpoints
Subdomain routing (future)
Set Initial Plan
Assign starting subscription plan: export type WorkspacePlan = "free" | "pro" | "enterprise" ;
Default: "free"
Complete Onboarding
Mark onboarding as complete: onboardingCompleted : boolean ( "onboarding_completed" ). default ( false )
Users are redirected to dashboard after completion.
Workspace Members
Workspaces support team collaboration with role-based access control.
User Roles
export type UserRole = "owner" | "admin" | "member" ;
Full Control Permissions:
Manage billing
Delete workspace
Invite/remove any member
Assign admin role
Access all projects
Modify workspace settings
role : text ( "role" ). notNull (). default ( "owner" )
Each workspace must have at least one owner.
Management Access Permissions:
Invite/remove members
Access all projects
Modify workspace settings
View billing (cannot modify)
Cannot:
Delete workspace
Remove owners
Modify billing settings
Standard Access Permissions:
Create own projects
View shared projects
Collaborate on team projects
Cannot:
Invite users
Modify workspace settings
Access billing
View other members’ private projects
Invitation System
Invite team members via email:
// Source: lib/db/schema.ts
export const invitation = pgTable ( "invitation" , {
id: text ( "id" ). primaryKey (),
email: text ( "email" ). notNull (),
workspaceId: text ( "workspace_id" ). notNull (),
role: text ( "role" ). notNull (). default ( "member" ),
token: text ( "token" ). notNull (). unique (),
expiresAt: timestamp ( "expires_at" ). notNull (),
acceptedAt: timestamp ( "accepted_at" ),
});
Send Invitation
Owner or admin invites a user: const invitation = await createInvitation ({
email: "[email protected] " ,
workspaceId: workspace . id ,
role: "member" ,
expiresAt: addDays ( new Date (), 7 ), // 7-day expiration
});
Email Notification
Recipient receives invitation email with:
Workspace name
Inviter name
Role assignment
Acceptance link with token
const acceptUrl = ` ${ baseUrl } /accept-invitation?token= ${ invitation . token } ` ;
Accept Invitation
User clicks link and either:
Existing User : Added to workspace
New User : Creates account, then added
acceptedAt : timestamp ( "accepted_at" )
Join Workspace
User’s account is updated: await updateUser ( userId , {
workspaceId: invitation . workspaceId ,
role: invitation . role ,
});
Invitation links expire after 7 days. Expired invitations must be resent.
Workspace Settings
Edit workspace information: < Form >
< Input name = "name" label = "Company Name" />
< Input name = "contactEmail" label = "Contact Email" />
< Input name = "contactPerson" label = "Contact Person" />
< Input name = "organizationNumber" label = "Org Number" />
</ Form >
All fields can be updated by owners and admins. Workspace slugs can be changed: slug : text ( "slug" ). notNull (). unique ()
Changing the slug will break existing bookmarks and links.
White-Label Branding
Customize the workspace appearance:
Logo
Brand Colors
Preview
Upload a custom logo: logo : text ( "logo" ), // URL to uploaded logo image
Logo specifications:
Format: PNG, SVG (transparent background recommended)
Size: 200x60px optimal
Max file size: 2MB
Displayed in:
Dashboard header
Email templates
Shared project pages
Set custom color scheme: primaryColor : text ( "primary_color" ), // Buttons, links
secondaryColor : text ( "secondary_color" ), // Accents, highlights
Format: HEX color codes (e.g., #3B82F6) Applied to:
UI components
Buttons and CTAs
Status badges
Charts and graphs
Live preview of branding changes before saving: < BrandingPreview
logo = { logo }
primaryColor = { primaryColor }
secondaryColor = { secondaryColor }
/>
Data Isolation
All data is strictly isolated by workspace:
Project Isolation
// Source: lib/db/schema.ts
export const project = pgTable ( "project" , {
id: text ( "id" ). primaryKey (),
workspaceId: text ( "workspace_id" )
. notNull ()
. references (() => workspace . id , { onDelete: "cascade" }),
userId: text ( "user_id" )
. notNull ()
. references (() => user . id , { onDelete: "cascade" }),
});
Projects are automatically deleted when a workspace is deleted (onDelete: "cascade").
Query Scoping
All database queries are scoped to the current workspace:
// Source: lib/db/queries.ts
export async function getProjects ( workspaceId : string ) {
return await db
. select ()
. from ( project )
. where ( eq ( project . workspaceId , workspaceId ))
. orderBy ( desc ( project . createdAt ));
}
File Storage Isolation
Files are organized by workspace in storage:
// Source: lib/supabase.ts
export function getImagePath (
workspaceId : string ,
projectId : string ,
fileName : string
) : string {
return ` ${ workspaceId } / ${ projectId } / ${ fileName } ` ;
}
Storage Structure:
storage/
├── workspace-123/
│ ├── project-abc/
│ │ ├── original/
│ │ │ ├── image-1.jpg
│ │ │ └── image-2.jpg
│ │ └── result/
│ │ ├── image-1.jpg
│ │ └── image-2.jpg
│ └── project-def/
└── workspace-456/
Billing & Plans
Subscription Plans
export type WorkspacePlan = "free" | "pro" | "enterprise" ;
Free Plan
Pro Plan
Enterprise Plan
Included:
5 projects per month
50 images per month
2 videos per month
1 team member
Community support
plan : text ( "plan" ). notNull (). default ( "free" )
Included:
Unlimited projects
Pay-per-use pricing
Up to 10 team members
Priority support
Custom branding
Invoice billing (eligible customers)
Pricing:
$99 per photo project (up to 10 images)
$0.35 per video clip
Custom:
Volume discounts
Unlimited team members
Dedicated support
SLA guarantees
Custom integrations
On-premise options
Contact sales for pricing.
Payment Methods
Stripe Pay-per-project with credit card: // Source: lib/db/schema.ts
export const projectPayment = pgTable ( "project_payment" , {
id: text ( "id" ). primaryKey (),
projectId: text ( "project_id" ). notNull (),
paymentMethod: text ( "payment_method" ), // "stripe"
stripeCheckoutSessionId: text ( "stripe_checkout_session_id" ),
amountCents: integer ( "amount_cents" ). notNull (),
status: text ( "status" ). notNull (). default ( "pending" ),
});
Flow:
Create project
Checkout redirect
Payment confirmation
Processing starts
Invoice Monthly invoicing for Norwegian B2B customers: invoiceEligible : boolean ( "invoice_eligible" ). default ( false ),
invoiceEligibleAt : timestamp ( "invoice_eligible_at" ),
Requirements:
Valid organization number
Norwegian company
Admin approval
Billing cycle: Monthly, NET 30
Invoice Management
// Source: lib/db/schema.ts
export const invoice = pgTable ( "invoice" , {
id: text ( "id" ). primaryKey (),
workspaceId: text ( "workspace_id" ). notNull (),
totalAmountOre: integer ( "total_amount_ore" ). notNull (), // Amount in øre
status: text ( "status" ). notNull (). default ( "draft" ),
issueDate: timestamp ( "issue_date" ),
dueDate: timestamp ( "due_date" ),
paidAt: timestamp ( "paid_at" ),
});
export const invoiceLineItem = pgTable ( "invoice_line_item" , {
id: text ( "id" ). primaryKey (),
invoiceId: text ( "invoice_id" ),
projectId: text ( "project_id" ),
description: text ( "description" ). notNull (),
amountOre: integer ( "amount_ore" ). notNull (),
status: text ( "status" ). notNull (). default ( "pending" ),
});
Usage Tracking
Line items created automatically: // When project starts processing
await createInvoiceLineItem ({
workspaceId ,
projectId ,
description: `Photo Project: ${ projectName } ` ,
amountOre: 99000 , // 990 NOK
status: "pending" ,
});
Monthly Billing
At month end, pending items grouped into invoice: const pendingItems = await getPendingLineItems ( workspaceId );
const totalAmount = pendingItems . reduce (
( sum , item ) => sum + item . amountOre ,
0
);
await createInvoice ({
workspaceId ,
totalAmountOre: totalAmount ,
status: "draft" ,
});
Fiken Integration
Invoice synced to Fiken accounting system: fikenInvoiceId : integer ( "fiken_invoice_id" ),
fikenInvoiceNumber : text ( "fiken_invoice_number" ),
Payment
Customer pays via bank transfer, status updated: status : "paid" ,
paidAt : timestamp ( "paid_at" ),
Workspace Switching
Users can belong to multiple workspaces:
// User can be invited to multiple workspaces
// Each workspace association has its own role
Currently, users have one primary workspace. Multi-workspace support is planned for a future release.
Admin Features
System Admins
Special users with cross-workspace access:
// Source: lib/db/schema.ts
export const user = pgTable ( "user" , {
// ...
isSystemAdmin: boolean ( "is_system_admin" ). default ( false ),
});
System Admin Powers:
View all workspaces
Impersonate users
Suspend workspaces
Modify billing
Access audit logs
Workspace Suspension
Admins can suspend workspaces:
status : text ( "status" ). notNull (). default ( "active" ),
suspendedAt : timestamp ( "suspended_at" ),
suspendedReason : text ( "suspended_reason" ),
Suspension Effects:
Cannot create projects
Cannot process images
Cannot generate videos
Read-only access to existing content
Cannot invite members
Common Reasons:
Payment failure
Terms of service violation
Account security issues
Manual admin action
Best Practices
Single Company = Single Workspace Even if you have multiple offices or teams, use one workspace with proper team member roles. Exceptions:
Separate legal entities
Different billing requirements
Complete data isolation needed
Follow the principle of least privilege:
Owners : 1-2 key decision makers
Admins : Department heads, team leads
Members : Everyone else
Regularly audit role assignments.
For consistent branding:
Use company logo (not personal photos)
Choose colors that match your brand
Test color contrast for accessibility
Keep logo files updated
Use company email addresses
Set appropriate roles before sending
Remove inactive members
Resend expired invitations promptly
Security
Data Encryption
Access Control
Audit Logs
All workspace data is encrypted:
At rest : Database encryption
In transit : TLS 1.3
File storage : Server-side encryption
Row-level security enforced: -- Example RLS policy
CREATE POLICY workspace_isolation ON project
USING (workspace_id = current_setting( 'app.current_workspace_id' ));
Critical actions are logged:
User invitations
Role changes
Workspace settings updates
Billing events
Suspension actions