---
title: "Provision Free and Pro SaaS users"
description: "Create Free and Pro quota plans and provision one application user onto either plan."
---

> Documentation Index
> Fetch the complete documentation index at: https://nexus.microvoid.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Provision Free and Pro SaaS users

## Goal

用应用后端创建 Free/Pro Plan，并把两个稳定的应用用户 ID 分配到对应 Access。

## Outcome

每名用户得到独立的 Better Auth User、WorkspaceAccess、当前 QuotaPeriod 和一次性 Inference Key。最终用户不是 Organization Member，也不获得 Admin 权限。

## Prerequisites

- 一个 Workspace ID 和可写该 Workspace 的 Management Key。
- Node.js 22.12+。
- 生产环境中的安全凭证交付通道。

## Lifecycle note

`WorkspacePlan` 是稳定套餐；发布后的 QuotaStrategy 通过 Binding 生效。Provision 以 `(workspaceId, externalUserId)` 标识应用用户。Key、Access 和 Period 是三个独立生命周期。

## 1. Configure the backend

```sh title="Terminal"
export NEXUS_ADMIN_BASE_URL="https://nexus.microvoid.io"
export NEXUS_MANAGEMENT_KEY="<NEXUS_MANAGEMENT_KEY>"
export NEXUS_WORKSPACE_ID="<WORKSPACE_ID>"
```

## 2. Run the complete provisioning program

```js title="provision-saas-users.mjs"
import { writeFile } from 'node:fs/promises';

const baseUrl = required('NEXUS_ADMIN_BASE_URL').replace(/\/$/, '');
const managementKey = required('NEXUS_MANAGEMENT_KEY');
const workspaceId = required('NEXUS_WORKSPACE_ID');

async function admin(path, { method = 'GET', body } = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${managementKey}`,
      ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error(`${method} ${path} failed: ${response.status} ${await response.text()}`);
  return response.status === 204 ? undefined : response.json();
}

async function createActivePlan(definition) {
  const plan = await admin(`/admin/v1/workspaces/${workspaceId}/plans`, {
    method: 'POST',
    body: { key: definition.key, name: definition.name, billingInterval: 'MONTH' },
  });
  const strategy = await admin(`/admin/v1/workspaces/${workspaceId}/plan-strategies/quota`, {
    method: 'POST',
    body: {
      key: `${definition.key}-quota`,
      resetInterval: 'MONTH',
      quotaUnits: definition.quotaUnits,
      inputMultiplierPpm: '1000000',
      outputMultiplierPpm: '2000000',
      cachedInputMultiplierPpm: '200000',
      maxOutputTokens: definition.maxOutputTokens,
    },
  });
  await admin(`/admin/v1/plan-strategies/${strategy.id}/publish`, { method: 'POST' });
  await admin(`/admin/v1/plans/${plan.id}/strategy-bindings`, {
    method: 'POST',
    body: { strategyId: strategy.id, effectiveFrom: new Date().toISOString() },
  });
  return admin(`/admin/v1/plans/${plan.id}/publish`, { method: 'POST' });
}

async function provision(planId, user) {
  return admin(`/admin/v1/workspaces/${workspaceId}/users`, {
    method: 'POST',
    body: { ...user, planId, issueInferenceKey: true },
  });
}

const free = await createActivePlan({ key: 'free', name: 'Free', quotaUnits: '1000000', maxOutputTokens: 2048 });
const pro = await createActivePlan({ key: 'pro', name: 'Pro', quotaUnits: '50000000', maxOutputTokens: 8192 });
const freeUser = await provision(free.id, {
  externalUserId: 'app-user-free-001',
  email: 'free-user@example.com',
  name: 'Free example user',
});
const proUser = await provision(pro.id, {
  externalUserId: 'app-user-pro-001',
  email: 'pro-user@example.com',
  name: 'Pro example user',
});

await writeFile(
  'provisioned-inference-keys.json',
  JSON.stringify(
    {
      free: { accessId: freeUser.workspaceAccess.id, apiKey: freeUser.apiKey },
      pro: { accessId: proUser.workspaceAccess.id, apiKey: proUser.apiKey },
    },
    null,
    2,
  ),
  { mode: 0o600, flag: 'wx' },
);
console.log(JSON.stringify({ freePlanId: free.id, proPlanId: pro.id, keyFile: 'provisioned-inference-keys.json' }));

function required(name) {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`${name} is required`);
  return value;
}
```

```sh title="Terminal"
node provision-saas-users.mjs
```

示例额度用于说明流程，不是产品定价建议；Quota Unit 表达 AI 使用权，不是钱。

## Check your work

- 两个 Plan 为 `ACTIVE`，各绑定一个已发布 QuotaStrategy。
- 两条 WorkspaceAccess 的 `externalUserId`、`planId` 和当前 QuotaPeriod 正确。
- `provisioned-inference-keys.json` 权限为 `0600`，每个 Key 明文只来自创建响应。

## Failure modes

- `PLAN_NOT_ACTIVE`：先发布 Strategy、创建 Binding，再发布 Plan。
- 重跑遇到唯一性冲突：不要用新的外部用户 ID 绕开；按 API 契约读取既有资源或采用应用自己的幂等工作流。
- Provision 响应没有 `apiKey`：已有 Access 不会重放旧 Key 明文；显式创建新 Inference Key。

## Security notes

Management Key 只在应用后端使用。处理完 Key 文件后，经批准的 Secret 交付通道传给用户并安全删除临时副本；不要写入日志、Analytics 或数据库明文字段。

## Next steps

把 [OpenAI JavaScript](/cookbook/openai-javascript)、[Python](/cookbook/openai-python) 或 [OpenCode](/cookbook/opencode) Recipe 交给最终用户。

Source: https://nexus.microvoid.io/cookbook/provision-saas-user/index.mdx
