Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
356 lines
9.5 KiB
Plaintext
356 lines
9.5 KiB
Plaintext
---
|
|
label: "Team Account Creation Policies"
|
|
title: "Guarding Team Account Creation with Policies"
|
|
description: "Learn how to restrict and validate team account creation using the policy system."
|
|
order: 8
|
|
---
|
|
|
|
The Team Account Creation Policies system allows you to define custom business rules that guard when users can create new team accounts using the [Policies API](../api/policies-api).
|
|
|
|
Common use cases include:
|
|
|
|
- Requiring an active subscription to create team accounts
|
|
- Requiring a specific subscription plan (e.g., Pro or Enterprise)
|
|
- Limiting the number of team accounts per user
|
|
|
|
{% sequence title="Implementation Steps" description="How to implement team account creation policies" %}
|
|
|
|
[Understanding Policies](#understanding-policies)
|
|
|
|
[Registering Policies](#registering-policies)
|
|
|
|
[Common Policy Examples](#common-policy-examples)
|
|
|
|
[Evaluating Policies](#evaluating-policies)
|
|
|
|
{% /sequence %}
|
|
|
|
## Understanding Policies
|
|
|
|
Policies are defined using the `definePolicy` function and registered in the `createAccountPolicyRegistry`. Each policy:
|
|
|
|
1. Has a unique ID
|
|
2. Specifies which stages it runs at (`preliminary` or `submission`)
|
|
3. Returns `allow()` or `deny()` with an error message
|
|
|
|
```typescript
|
|
import { allow, definePolicy, deny } from '@kit/policies';
|
|
import type { FeaturePolicyCreateAccountContext } from '@kit/team-accounts/server';
|
|
|
|
const myPolicy = definePolicy<FeaturePolicyCreateAccountContext>({
|
|
id: 'my-policy-id',
|
|
stages: ['preliminary', 'submission'],
|
|
|
|
async evaluate(context) {
|
|
// Return allow() to permit the action
|
|
// Return deny({ code, message, remediation }) to block it
|
|
},
|
|
});
|
|
```
|
|
|
|
### Policy Stages
|
|
|
|
- **preliminary**: Runs before showing the create account form. Use to check if the user can attempt to create an account.
|
|
- **submission**: Runs when the form is submitted. Use to validate the account name and final checks.
|
|
|
|
## Registering Policies
|
|
|
|
Create a setup file and import it in your layout to register policies at app startup.
|
|
|
|
### Step 1: Create the Registration File
|
|
|
|
```typescript
|
|
// apps/web/lib/policies/setup-create-account-policies.ts
|
|
import 'server-only';
|
|
|
|
import { createAccountPolicyRegistry } from '@kit/team-accounts/server';
|
|
|
|
import { subscriptionRequiredPolicy } from './create-account-policies';
|
|
|
|
createAccountPolicyRegistry.registerPolicy(subscriptionRequiredPolicy);
|
|
```
|
|
|
|
### Step 2: Import in Layout
|
|
|
|
```typescript
|
|
// apps/web/app/home/layout.tsx
|
|
import '~/lib/policies/setup-create-account-policies';
|
|
|
|
export default function HomeLayout({ children }) {
|
|
return <>{children}</>;
|
|
}
|
|
```
|
|
|
|
{% callout type="default" title="Default Behavior" %}
|
|
By default, no policies are registered and all users can create team accounts. You must register policies to enforce restrictions.
|
|
{% /callout %}
|
|
|
|
## Common Policy Examples
|
|
|
|
### Require Active Subscription
|
|
|
|
Block team account creation unless the user has an active subscription on their personal account:
|
|
|
|
```typescript
|
|
// apps/web/lib/policies/create-account-policies.ts
|
|
import 'server-only';
|
|
|
|
import { allow, definePolicy, deny } from '@kit/policies';
|
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|
|
|
import type { FeaturePolicyCreateAccountContext } from '@kit/team-accounts/server';
|
|
|
|
export const subscriptionRequiredPolicy =
|
|
definePolicy<FeaturePolicyCreateAccountContext>({
|
|
id: 'subscription-required',
|
|
stages: ['preliminary', 'submission'],
|
|
|
|
async evaluate(context) {
|
|
const client = getSupabaseServerClient();
|
|
|
|
const { data: subscription, error } = await client
|
|
.from('subscriptions')
|
|
.select('id, status, active')
|
|
.eq('account_id', context.userId)
|
|
.eq('active', true)
|
|
.maybeSingle();
|
|
|
|
if (error) {
|
|
return deny({
|
|
code: 'SUBSCRIPTION_CHECK_FAILED',
|
|
message: 'Failed to verify subscription status',
|
|
});
|
|
}
|
|
|
|
if (!subscription) {
|
|
return deny({
|
|
code: 'SUBSCRIPTION_REQUIRED',
|
|
message: 'An active subscription is required to create team accounts',
|
|
remediation: 'Please upgrade your plan to create team accounts',
|
|
});
|
|
}
|
|
|
|
return allow();
|
|
},
|
|
});
|
|
```
|
|
|
|
### Require Specific Plan (Price ID)
|
|
|
|
Only allow users with a specific subscription plan to create team accounts:
|
|
|
|
```typescript
|
|
export const proPlanRequiredPolicy = definePolicy<
|
|
FeaturePolicyCreateAccountContext,
|
|
{ allowedPriceIds: string[] }
|
|
>({
|
|
id: 'pro-plan-required',
|
|
stages: ['preliminary', 'submission'],
|
|
|
|
async evaluate(context, config) {
|
|
const allowedPriceIds = config?.allowedPriceIds ?? [
|
|
'price_pro_monthly',
|
|
'price_pro_yearly',
|
|
'price_enterprise_monthly',
|
|
'price_enterprise_yearly',
|
|
];
|
|
|
|
const client = getSupabaseServerClient();
|
|
|
|
const { data: subscription, error } = await client
|
|
.from('subscriptions')
|
|
.select('id, active, subscription_items(price_id)')
|
|
.eq('account_id', context.userId)
|
|
.eq('active', true)
|
|
.maybeSingle();
|
|
|
|
if (error) {
|
|
return deny({
|
|
code: 'SUBSCRIPTION_CHECK_FAILED',
|
|
message: 'Failed to verify subscription status',
|
|
});
|
|
}
|
|
|
|
if (!subscription) {
|
|
return deny({
|
|
code: 'SUBSCRIPTION_REQUIRED',
|
|
message: 'A subscription is required to create team accounts',
|
|
remediation: 'Please subscribe to a plan to create team accounts',
|
|
});
|
|
}
|
|
|
|
const priceIds =
|
|
subscription.subscription_items?.map((item) => item.price_id) ?? [];
|
|
|
|
const hasAllowedPlan = priceIds.some((priceId) =>
|
|
allowedPriceIds.includes(priceId ?? '')
|
|
);
|
|
|
|
if (!hasAllowedPlan) {
|
|
return deny({
|
|
code: 'PLAN_NOT_ALLOWED',
|
|
message: 'Your current plan does not include team account creation',
|
|
remediation: 'Please upgrade to a Pro or Enterprise plan',
|
|
});
|
|
}
|
|
|
|
return allow();
|
|
},
|
|
});
|
|
```
|
|
|
|
### Maximum Accounts Per User
|
|
|
|
Limit how many team accounts a user can own:
|
|
|
|
```typescript
|
|
export const maxAccountsPolicy = definePolicy<
|
|
FeaturePolicyCreateAccountContext,
|
|
{ maxAccounts: number }
|
|
>({
|
|
id: 'max-accounts-per-user',
|
|
stages: ['preliminary', 'submission'],
|
|
|
|
async evaluate(context, config) {
|
|
const maxAccounts = config?.maxAccounts ?? 3;
|
|
|
|
const client = getSupabaseServerClient();
|
|
|
|
const { count, error } = await client
|
|
.from('accounts')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('primary_owner_user_id', context.userId)
|
|
.eq('is_personal_account', false);
|
|
|
|
if (error) {
|
|
return deny({
|
|
code: 'MAX_ACCOUNTS_CHECK_FAILED',
|
|
message: 'Failed to check account count',
|
|
});
|
|
}
|
|
|
|
const currentCount = count ?? 0;
|
|
|
|
if (currentCount >= maxAccounts) {
|
|
return deny({
|
|
code: 'MAX_ACCOUNTS_REACHED',
|
|
message: `You have reached the maximum of ${maxAccounts} team accounts`,
|
|
remediation: 'Delete an existing team account to create a new one',
|
|
});
|
|
}
|
|
|
|
return allow();
|
|
},
|
|
});
|
|
```
|
|
|
|
### Rate Limiting Account Creation
|
|
|
|
Prevent users from creating too many accounts in a short period:
|
|
|
|
```typescript
|
|
export const rateLimitPolicy = definePolicy<
|
|
FeaturePolicyCreateAccountContext,
|
|
{ maxAccountsPerDay: number }
|
|
>({
|
|
id: 'account-creation-rate-limit',
|
|
stages: ['submission'],
|
|
|
|
async evaluate(context, config) {
|
|
const maxAccountsPerDay = config?.maxAccountsPerDay ?? 5;
|
|
|
|
const client = getSupabaseServerClient();
|
|
|
|
const oneDayAgo = new Date();
|
|
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
|
|
|
const { count, error } = await client
|
|
.from('accounts')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('primary_owner_user_id', context.userId)
|
|
.eq('is_personal_account', false)
|
|
.gte('created_at', oneDayAgo.toISOString());
|
|
|
|
if (error) {
|
|
return deny({
|
|
code: 'RATE_LIMIT_CHECK_FAILED',
|
|
message: 'Failed to check rate limit',
|
|
});
|
|
}
|
|
|
|
if ((count ?? 0) >= maxAccountsPerDay) {
|
|
return deny({
|
|
code: 'RATE_LIMIT_EXCEEDED',
|
|
message: `You can only create ${maxAccountsPerDay} accounts per day`,
|
|
remediation: 'Please wait 24 hours before creating another account',
|
|
});
|
|
}
|
|
|
|
return allow();
|
|
},
|
|
});
|
|
```
|
|
|
|
### Combining Multiple Policies
|
|
|
|
Register multiple policies to enforce several rules:
|
|
|
|
```typescript
|
|
// apps/web/lib/policies/setup-create-account-policies.ts
|
|
import 'server-only';
|
|
|
|
import { createAccountPolicyRegistry } from '@kit/team-accounts/server';
|
|
|
|
import {
|
|
maxAccountsPolicy,
|
|
proPlanRequiredPolicy,
|
|
rateLimitPolicy,
|
|
} from './create-account-policies';
|
|
|
|
createAccountPolicyRegistry
|
|
.registerPolicy(proPlanRequiredPolicy)
|
|
.registerPolicy(maxAccountsPolicy)
|
|
.registerPolicy(rateLimitPolicy);
|
|
```
|
|
|
|
## Evaluating Policies
|
|
|
|
Use `createAccountCreationPolicyEvaluator` to check policies in your server actions:
|
|
|
|
```typescript
|
|
import { createAccountCreationPolicyEvaluator } from '@kit/team-accounts/server';
|
|
|
|
async function checkCanCreateAccount(userId: string) {
|
|
const evaluator = createAccountCreationPolicyEvaluator();
|
|
|
|
const result = await evaluator.canCreateAccount(
|
|
{
|
|
userId,
|
|
accountName: '',
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
'preliminary'
|
|
);
|
|
|
|
return {
|
|
allowed: result.allowed,
|
|
reason: result.reasons[0] ?? null,
|
|
};
|
|
}
|
|
```
|
|
|
|
### Checking if Policies Exist
|
|
|
|
Before running evaluations, you can check if any policies are registered:
|
|
|
|
```typescript
|
|
const evaluator = createAccountCreationPolicyEvaluator();
|
|
|
|
const hasPolicies = await evaluator.hasPoliciesForStage('preliminary');
|
|
|
|
if (hasPolicies) {
|
|
const result = await evaluator.canCreateAccount(context, 'preliminary');
|
|
// Handle result...
|
|
}
|
|
```
|