Skip to main content

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:
1

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"),
});
2

Company Information

Collect organization details:
  • Company name
  • Contact email
  • Contact person name
organizationNumber: text("organization_number"),
contactEmail: text("contact_email"),
contactPerson: text("contact_person"),
3

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)
4

Set Initial Plan

Assign starting subscription plan:
export type WorkspacePlan = "free" | "pro" | "enterprise";
Default: "free"
5

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 ControlPermissions:
  • 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.

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"),
});
1

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
});
2

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}`;
3

Accept Invitation

User clicks link and either:
  • Existing User: Added to workspace
  • New User: Creates account, then added
acceptedAt: timestamp("accepted_at")
4

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

Basic Information

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.

White-Label Branding

Customize the workspace appearance:

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";
Included:
  • 5 projects per month
  • 50 images per month
  • 2 videos per month
  • 1 team member
  • Community support
plan: text("plan").notNull().default("free")

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:
  1. Create project
  2. Checkout redirect
  3. Payment confirmation
  4. 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"),
});
1

Usage Tracking

Line items created automatically:
// When project starts processing
await createInvoiceLineItem({
  workspaceId,
  projectId,
  description: `Photo Project: ${projectName}`,
  amountOre: 99000, // 990 NOK
  status: "pending",
});
2

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",
});
3

Fiken Integration

Invoice synced to Fiken accounting system:
fikenInvoiceId: integer("fiken_invoice_id"),
fikenInvoiceNumber: text("fiken_invoice_number"),
4

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 WorkspaceEven 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

All workspace data is encrypted:
  • At rest: Database encryption
  • In transit: TLS 1.3
  • File storage: Server-side encryption

Build docs developers (and LLMs) love