Multi-tenant applications with Clerk Organizations for Google Antigravity projects including team management, roles, and permissions.
# Clerk Organizations Multi-Tenancy for Google Antigravity
Build multi-tenant applications with Clerk Organizations in your Google Antigravity IDE projects. This comprehensive guide covers organization management, role-based access control, and team collaboration patterns optimized for Gemini 3 agentic development.
## Clerk Configuration
Set up Clerk with organization support:
```typescript
// lib/clerk.ts
import { clerkClient } from '@clerk/nextjs/server';
// Organization role definitions
export const ORGANIZATION_ROLES = {
owner: {
name: 'Owner',
permissions: ['*'],
description: 'Full access to organization settings and resources',
},
admin: {
name: 'Admin',
permissions: [
'org:manage_members',
'org:manage_settings',
'prompts:create',
'prompts:edit',
'prompts:delete',
'prompts:publish',
],
description: 'Manage members and all prompts',
},
member: {
name: 'Member',
permissions: [
'prompts:create',
'prompts:edit_own',
'prompts:delete_own',
],
description: 'Create and manage own prompts',
},
viewer: {
name: 'Viewer',
permissions: ['prompts:view'],
description: 'View-only access to prompts',
},
} as const;
export type OrganizationRole = keyof typeof ORGANIZATION_ROLES;
export function hasPermission(
userPermissions: string[],
requiredPermission: string
): boolean {
return (
userPermissions.includes('*') ||
userPermissions.includes(requiredPermission)
);
}
```
## Middleware Setup
Protect routes based on organization membership:
```typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/prompts(.*)',
'/api/public(.*)',
'/sign-in(.*)',
'/sign-up(.*)',
]);
const isOrgRoute = createRouteMatcher([
'/org/(.*)',
'/api/org/(.*)',
'/dashboard(.*)',
]);
export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) {
auth().protect();
}
if (isOrgRoute(request)) {
auth().protect({
// Require active organization for org routes
unauthenticatedUrl: '/sign-in',
unauthorizedUrl: '/select-organization',
});
}
});
export const config = {
matcher: ['/((?!.*\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
```
## Organization Context Provider
Manage organization state in React:
```typescript
// contexts/OrganizationContext.tsx
'use client';
import { createContext, useContext, useMemo } from 'react';
import { useOrganization, useOrganizationList } from '@clerk/nextjs';
import { ORGANIZATION_ROLES, hasPermission } from '@/lib/clerk';
interface OrganizationContextType {
organization: ReturnType<typeof useOrganization>['organization'];
membership: ReturnType<typeof useOrganization>['membership'];
isLoaded: boolean;
permissions: string[];
hasPermission: (permission: string) => boolean;
isOwner: boolean;
isAdmin: boolean;
}
const OrganizationContext = createContext<OrganizationContextType | null>(null);
export function OrganizationProvider({ children }: { children: React.ReactNode }) {
const { organization, membership, isLoaded } = useOrganization();
const role = membership?.role as keyof typeof ORGANIZATION_ROLES | undefined;
const permissions = role ? ORGANIZATION_ROLES[role]?.permissions ?? [] : [];
const value = useMemo(
() => ({
organization,
membership,
isLoaded,
permissions,
hasPermission: (permission: string) => hasPermission(permissions, permission),
isOwner: role === 'owner',
isAdmin: role === 'owner' || role === 'admin',
}),
[organization, membership, isLoaded, permissions, role]
);
return (
<OrganizationContext.Provider value={value}>
{children}
</OrganizationContext.Provider>
);
}
export function useOrg() {
const context = useContext(OrganizationContext);
if (!context) {
throw new Error('useOrg must be used within OrganizationProvider');
}
return context;
}
```
## Organization Management Components
Build team management interfaces:
```typescript
// components/organization/InviteMembers.tsx
'use client';
import { useState } from 'react';
import { useOrganization } from '@clerk/nextjs';
import { OrganizationRole, ORGANIZATION_ROLES } from '@/lib/clerk';
export function InviteMembers() {
const { organization } = useOrganization();
const [email, setEmail] = useState('');
const [role, setRole] = useState<OrganizationRole>('member');
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const handleInvite = async (e: React.FormEvent) => {
e.preventDefault();
if (!organization) return;
setIsLoading(true);
setMessage(null);
try {
await organization.inviteMember({
emailAddress: email,
role,
});
setMessage({ type: 'success', text: `Invitation sent to ${email}` });
setEmail('');
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to send invitation',
});
} finally {
setIsLoading(false);
}
};
return (
<div className="invite-members">
<h3>Invite Team Members</h3>
<form onSubmit={handleInvite}>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="colleague@company.com"
required
/>
</div>
<div className="form-group">
<label htmlFor="role">Role</label>
<select
id="role"
value={role}
onChange={(e) => setRole(e.target.value as OrganizationRole)}
>
{Object.entries(ORGANIZATION_ROLES).map(([key, config]) => (
<option key={key} value={key}>
{config.name} - {config.description}
</option>
))}
</select>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send Invitation'}
</button>
{message && (
<p className={`message ${message.type}`}>{message.text}</p>
)}
</form>
</div>
);
}
// components/organization/MembersList.tsx
'use client';
import { useOrganization } from '@clerk/nextjs';
import { useOrg } from '@/contexts/OrganizationContext';
export function MembersList() {
const { membershipList, isLoaded } = useOrganization({
membershipList: { limit: 100 },
});
const { isAdmin } = useOrg();
if (!isLoaded) return <MembersListSkeleton />;
return (
<div className="members-list">
<h3>Team Members ({membershipList?.length || 0})</h3>
<ul>
{membershipList?.map((membership) => (
<li key={membership.id} className="member-item">
<img
src={membership.publicUserData.imageUrl}
alt={membership.publicUserData.firstName || 'Member'}
className="avatar"
/>
<div className="member-info">
<span className="name">
{membership.publicUserData.firstName}{' '}
{membership.publicUserData.lastName}
</span>
<span className="role">{membership.role}</span>
</div>
{isAdmin && membership.role !== 'owner' && (
<MemberActions membership={membership} />
)}
</li>
))}
</ul>
</div>
);
}
```
## API Route Protection
Protect API routes with organization checks:
```typescript
// app/api/org/prompts/route.ts
import { auth } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { hasPermission, ORGANIZATION_ROLES } from '@/lib/clerk';
export async function POST(request: NextRequest) {
const { userId, orgId, orgRole } = auth();
if (!userId || !orgId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const role = orgRole as keyof typeof ORGANIZATION_ROLES;
const permissions = ORGANIZATION_ROLES[role]?.permissions ?? [];
if (!hasPermission(permissions, 'prompts:create')) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await request.json();
const prompt = await db.prompt.create({
data: {
...body,
organizationId: orgId,
authorId: userId,
},
});
return NextResponse.json(prompt);
}
```
## Best Practices
1. **Define clear role hierarchies** with specific permissions
2. **Use Clerk components** for organization switching UI
3. **Implement audit logging** for organization changes
4. **Cache organization data** to reduce API calls
5. **Handle organization switching** gracefully in UI
6. **Set up webhooks** for organization events
7. **Implement resource quotas** per organization tierThis clerk prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their clerk implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For clerk projects, consider mentioning your framework version, coding style, and any specific libraries you're using.