Skip to content

Cookbook

Cookbook

Practical, copy-pasteable recipes. Each assumes you resolve contracts from the container.

Central login across your products

Your products don't embed the package — they log in against the running instance and reconcile identity. Server-side, provisioning a federated login is one call:

use Cbox\Id\Identity\Contracts\Subjects;
use Cbox\Id\Identity\ValueObjects\FederatedPrincipal;

$user = app(Subjects::class)->provisionFederated(
    new FederatedPrincipal('oidc', 'google|123', '[email protected]', 'Sam'),
);

provisionFederated is idempotent per (provider, subject): the first call creates the user and the identity link; later calls return the same user.

Set up a reseller → customer hierarchy

use Cbox\Id\Organization\Contracts\Organizations;
use Cbox\Id\Organization\Contracts\OrganizationHierarchy;
use Cbox\Id\Organization\ValueObjects\NewOrganization;
use Cbox\Id\Organization\Enums\OrganizationType;

$orgs = app(Organizations::class);
$reseller = $orgs->create(new NewOrganization('Contoso Partners', 'contoso', OrganizationType::Reseller));
$customer = $orgs->create(new NewOrganization('Northwind', 'northwind', parentId: $reseller->id));

$h = app(OrganizationHierarchy::class);
$h->manages($reseller->id, $customer->id);   // true — the reseller manages the customer
$h->descendants($reseller->id);              // ['<northwind id>']

A support role granted at the reseller now applies to the customer:

use Cbox\Id\AccessControl\Contracts\Roles;
use Cbox\Id\AccessControl\Contracts\AccessChecker;

$role = app(Roles::class)->define($reseller->id, 'support');
app(Roles::class)->grantPermission($reseller->id, $role->id, 'tickets.manage');
app(Roles::class)->assign($reseller->id, 'support_1', $role->id);

app(AccessChecker::class)->can('support_1', 'tickets.manage', $customer->id); // true (rolls down)

Suspend or archive an organization — and gate on it

Both state changes go through the contract, never onto the model. Each writes the status, emits its domain event and records an audit entry attributed to the operator who performed it; both are idempotent.

use Cbox\Id\Organization\Contracts\Organizations;

$orgs = app(Organizations::class);

$orgs->suspend($org->id, $operatorId);      // reversible: organization.suspended
$orgs->reactivate($org->id, $operatorId);   //             organization.reactivated
$orgs->archive($org->id, $operatorId);      // terminal:   organization.archived

archive() sets OrganizationStatus::Deleted. It is a soft, terminal state — the rows stay for the audit trail and any regulatory hold — but it revokes access exactly as a suspension does.

Ask the status, do not compare it:

if ($org->status->revokesAccess()) {
    abort(403);
}

Do not write $status === OrganizationStatus::Suspended. That test is how a "deleted" organization kept authenticating its members, consenting on their behalf and minting tokens. revokesAccess() is an exhaustive match with no default, so a status added in a later release fails static analysis at your call site rather than silently inheriting "allowed".

The same shape applies one layer up, on the platform plane — Accounts::suspend() and PlatformOperators::suspend() both take the acting operator and audit internally:

use Cbox\Id\Platform\Contracts\Accounts;

app(Accounts::class)->suspend($accountId, $operatorId);   // account.suspended
app(Accounts::class)->reactivate($accountId, $operatorId); // account.reactivated

Suspending an account is the widest revocation available: its members stop signing in, its API keys stop resolving, and every environment it owns stops serving auth on the next request.

Push capability gates from Stripe / Cashier

Wire your billing webhook to the entitlement writer. Billing translates the plan into capability gates (Cbox ID never sees the plan itself). Use reconcile() to guard against dropped webhooks — it upserts what's present and revokes what's absent:

use Cbox\Id\Kernel\Authorization\Contracts\EntitlementWriter;
use Cbox\Id\Kernel\Authorization\Enums\EnforcementMode;
use Cbox\Id\Kernel\Authorization\Enums\EntitlementSource;
use Cbox\Id\Kernel\Authorization\ValueObjects\EntitlementInput;

app(EntitlementWriter::class)->reconcile($org->id, [
    new EntitlementInput('feature.sso', ['enabled' => true]),                   // gate, from the plan
    new EntitlementInput('feature.export', ['enabled' => true]),
    new EntitlementInput('seats', ['limit' => 50], EnforcementMode::DecisionApi), // limit, checked live
], EntitlementSource::Billing);

Pick EnforcementMode::Claims for coarse, slow-changing gates (embedded in tokens), and EnforcementMode::DecisionApi (the default) for anything that must revoke immediately.

Keeping your own billing engine and want the full flow — reconcile, enforcement modes, provenance and events? See Entitlements & billing.

Read an organization's usage

Metering counts what an org did (locally — analytics, not billing). Read it for a dashboard, or as the input to a soft gate:

use Cbox\Id\Kernel\Usage\Contracts\UsageMeter;

$meter = app(UsageMeter::class);
$since = now()->subDays(30);
$until = now();

$meter->snapshot($org->id, $since, $until);            // ['auth.login' => 74, 'auth.id_token' => 120, …]
$meter->total('auth.login', $org->id, $since, $until);  // 74
$meter->series('auth.login', $org->id, $since, $until); // ['2026-07-01' => 3, …] for a chart

// A soft gate is just: read the counter, compare to the plan's allowance.
if ($meter->total('auth.id_token', $org->id, now()->startOfMonth()) >= $monthlyTokenLimit) {
    // warn / throttle / prompt an upgrade
}

Metrics are recorded automatically off the outbox — you rarely call record() by hand. See Usage metering for the shared auth.* vocabulary and the metering-vs-billing boundary.

Provision users over SCIM (inbound — an IdP → the platform)

use Cbox\Id\Directory\Contracts\Directories;
use Cbox\Id\Directory\Contracts\DirectorySync;
use Cbox\Id\Directory\ValueObjects\ScimUser;

$registered = app(Directories::class)->register($org->id, 'Okta'); // token shown ONCE
$directory = $registered->directory;

app(DirectorySync::class)->provisionUser($directory->id, new ScimUser('okta|1', 'dana', '[email protected]'));

// Deprovision drops membership AND kills the user's sessions immediately:
app(DirectorySync::class)->deprovisionUser($directory->id, 'okta|1');

Provision users to a downstream app (outbound — the platform → a SaaS app)

The mirror direction: push the platform's user/membership changes OUT to an organization's downstream apps over their SCIM endpoints. See the full recipe: Provision users to a downstream app.

Send a one-time passcode (email / SMS)

use Cbox\Id\Otp\Contracts\OtpService;

$challenge = app(OtpService::class)->issue('login', '[email protected]', 'email', request()->ip());
// ... user types the code they received ...
$result = app(OtpService::class)->verify($challenge->id, $code, request()->ip());
$result->verified; // true once, then single-use

Email works out of the box. To offer "text me a code", wire your SMS provider behind the channel contract: Add an SMS OTP channel.

Vault a credential for an AI agent

Store a downstream API key sealed, authorize an agent, and lease it for a single call — the agent never holds the long-lived secret. See the full recipe: Vault a downstream credential. To require a human to approve a high-risk agent action first, see Approve agent actions with CIBA.

Review who has access (access certification)

Open a campaign, have reviewers certify or revoke each role/membership, and apply the revokes on close — pending items are removed by default. See the full recipe: Run an access review.

Enrich or veto a token with an inline hook

Add a custom claim to every access token — or block issuance — with an in-process action or an external HTTPS endpoint. See the full recipe: Add a token claims hook. The same machinery gates logins, signups and password changes: Hook points.

Register a webhook

use Cbox\Id\Webhooks\Contracts\WebhookRegistry;

$registered = app(WebhookRegistry::class)->register($org->id, 'https://app.acme.test/hooks', [
    'organization.created', 'user.login', 'entitlement.updated',
]);
// $registered->secret is the HMAC signing secret — store it once; verify X-Cbox-Signature.

Delivered domain events fan out automatically; failures retry with exponential backoff.

Verify the audit chain

use Cbox\Id\Kernel\Audit\Contracts\AuditLog;

$result = app(AuditLog::class)->verifyChain($org->id);
$result->valid;            // false if any entry was tampered, reordered or deleted
$result->brokenAtSequence; // where it broke

// Sign a checkpoint to anchor externally:
$checkpoint = app(AuditLog::class)->checkpoint($org->id);

Stream audit events to a customer's SIEM

Mirror one environment's hash-chained trail to Splunk, Elastic, Graylog or a CEF collector — isolation intact, dedup by the entry hash. See the full recipe: Stream audit events to a SIEM.