BetterIAM
Upgrade

Upgrade to 0.3

Every breaking and behavior change between Better IAM 0.2.0 and 0.3.x, with before and after code, the commands to run, and how to verify the upgrade.

better-iam@better-iam/server@better-iam/next@better-iam/react@better-iam/vue@better-iam/svelte@better-iam/nestjs@better-iam/react-routerCHANGELOG.mdcatalog.tscontext-keys.tsoptions.tsidentities.tspackage.jsonpackage.jsonpackage.jsonpackage.jsonpackage.jsonpackage.json

This guide moves a deployment from Better IAM 0.2.0 to 0.3.1, the latest 0.3 release. Version 0.3.0 added three access-governance domains (guest collaboration, license management, and security clearances) and stopped installing every web framework with the umbrella package. Version 0.3.1 fixes only the security clearances module. You will:

  • upgrade every better-iam and @better-iam/* package to the same version;
  • install your web framework explicitly if you relied on the integration packages to pull it in;
  • rename identity attributes and resolveContext keys that now collide with server-owned names;
  • run the migration and the deployment self-check;
  • schedule the new jobs, if you turn on the new features.

For most deployments the upgrade is a version bump plus npx better-iam migrate. The changes below matter only if your configuration uses one of the names that became reserved, or if you enable the new clearances option.

Summary of changes

ChangeVersionWho is affectedWhat to do
Framework peers of @better-iam/next, react, vue, svelte, nestjs, and react-router are optional0.3.0Apps that use a framework without listing it in their own package.jsonInstall the framework explicitly
guest, guestSponsorId, homeTenantId, and licenses are reserved principal names0.3.0Deployments that declare an identity attribute with one of these namesRename the attribute
principal.guest, principal.guestSponsorId, principal.homeTenantId, and principal.licenses are server-owned0.3.0Apps whose resolveContext or plugin returns one of these keysRename the key
identities.resendInvitation refuses an invitation that acceptance would refuse0.3.0Apps that resend member invitationsRevoke and invite again
Birthright, onboarding, and workflow rules that never test identity.guest no longer match guests0.3.0Deployments that start inviting guestsReview rules before inviting guests
Adding people to a group that holds license seats needs iam:licenses:assign0.3.0Deployments that assign license products to groupsGrant the permission
More reserved names, and root administrators no longer read up on labeled resources0.3.0Only deployments that set the new clearances optionClearances changes
Label and scheme rules tightened0.3.1Only deployments that set clearances on 0.3.00.3.0 to 0.3.1

There is no new schema step in 0.3: the built-in migration steps are the same six as in 0.2.0 (0001_records through 0006_protection_indexes), and the new collections are records in the existing tables. Running the migration is still required, because it is how every release applies plugin migrations and confirms the schema.

Prerequisites

  • A deployment running Better IAM 0.2.0. Coming from 0.1, read Upgrade guides first and upgrade in order.
  • Access to run the better-iam CLI against the deployment's configuration (the same better-iam.config.mjs your application loads), with the BETTER_IAM_SECRET and database environment variables set.
  • A backup or snapshot of the database, for example npx better-iam store-export --config better-iam.config.mjs --output backup.jsonl; see Database operations.

Upgrade steps

Update every package to 0.3.1

Better IAM packages are released together at one version. Upgrade them all at once, never one package at a time.

With the umbrella package:

npm i better-iam@0.3.1

With individual packages, list each one you use at the same version:

npm i @better-iam/server@0.3.1 @better-iam/client@0.3.1 @better-iam/adapter-postgres@0.3.1 @better-iam/next@0.3.1

The Nuxt module, @better-iam/nuxt, is not part of the umbrella package; upgrade it to the same version if you use it.

Install your framework explicitly

If your application imports a framework (Next.js, React, Vue, Svelte, SvelteKit, NestJS, or React Router) but does not list it in its own dependencies, add it now. See Framework peer dependencies are optional.

npm i next react react-dom

Rename reserved names

Search your configuration for identity attributes named guest, guestSponsorId, homeTenantId, or licenses, and your resolveContext and plugins for the matching principal.* keys. Rename them as shown in Reserved principal names. A configuration that still declares one fails at construction with INVALID_CONFIG, so this step must happen before the deploy.

Run the migration

npx better-iam migrate --config better-iam.config.mjs

It prints Database and plugin migrations applied. and is safe to run again. From code, iam.initialize() does the same; see Migrations.

Check the deployment

npx better-iam doctor --config better-iam.config.mjs --strict

doctor reports the schema version, applied migrations, record counts, and configuration findings such as a missing email transport or jobs that are not scheduled. With --strict it exits non-zero (DOCTOR_FINDINGS) on any error or warning, such as schema-behind, so it can gate a pipeline. See Gate the deploy with doctor.

Schedule the new jobs (only for features you use)

The three new domains add instance jobs. Schedule the ones for features you turn on; see Scheduled jobs.

FeatureJobHow often
Guestsiam.guests.sweep(), then iam.guests.sendReviewReminders()Hourly, then the outbox
Licensesiam.licenses.reconcile()Hourly
Licensesiam.licenses.reclaim()Daily
Security clearancesiam.clearances.sendReminders()Daily, then the outbox
jobs.ts
import { iam } from './iam';

setInterval(async () => {
  await iam.guests.sweep();
  await iam.guests.sendReviewReminders();
  await iam.licenses.reconcile();
  await iam.auth.dispatchOutbox();
}, 60 * 60_000).unref();

setInterval(async () => {
  await iam.licenses.reclaim();
}, 24 * 60 * 60_000).unref();

Deploy

Deploy the new version after the migration has run. No 0.3 change invalidates sessions, API keys, or tokens issued by 0.2.0, so people should not have to sign in again; step 4 of Verify it works checks it.

Breaking changes

Framework peer dependencies are optional

What changed. @better-iam/next, @better-iam/react, @better-iam/vue, @better-iam/svelte, @better-iam/nestjs, and @better-iam/react-router now mark their framework peers as optional in peerDependenciesMeta. npm, pnpm, and Yarn therefore no longer install Next.js, React, Vue, Svelte, NestJS, or React Router automatically. The umbrella package already did this in 0.2.0, so installing better-iam alone no longer installs every framework either.

Who is affected. An application that uses a framework only because an integration package installed it, without listing the framework in its own package.json. After the upgrade, imports such as import { useState } from 'react' fail to resolve.

PackageOptional peers
@better-iam/nextnext, react
@better-iam/reactreact
@better-iam/vuevue
@better-iam/sveltesvelte, @sveltejs/kit
@better-iam/nestjs@nestjs/common, @nestjs/core, reflect-metadata, rxjs
@better-iam/react-routerreact-router
package.json
{
  "dependencies": {
    "@better-iam/next": "0.2.0",
    "@better-iam/server": "0.2.0"
  }
}

The peer ranges themselves did not change: @better-iam/next still expects next ^15.0.0 and react ^19.0.0.

Reserved principal names

What changed. Four names are now reserved for values the server sets on every decision:

NamePolicy keySet by
guestprincipal.guestGuests: true while the identity is a guest of the deciding tenant
guestSponsorIdprincipal.guestSponsorIdGuests: the guest's sponsor
homeTenantIdprincipal.homeTenantIdGuests: the tenant that verified the guest's email domain
licensesprincipal.licensesLicenses: keys of the products the principal holds an active seat for

Who is affected. A deployment that declares an identity attribute with one of these names. Construction fails with INVALID_CONFIG and the message identityAttributes cannot redefine principal.licenses (or the name you used).

iam.ts
import { betterIam } from 'better-iam';
import { postgresAdapter } from 'better-iam/adapter-postgres';

export const iam = betterIam({
  database: postgresAdapter({ connectionString: process.env.DATABASE_URL! }),
  secret: process.env.BETTER_IAM_SECRET!,
  baseURL: process.env.BETTER_IAM_BASE_URL!,
  permissions: {
    identityAttributes: { department: 'string', licenses: 'string' },
  },
});

After renaming, copy the stored values to the new attribute with identities.update(credential, { tenantId, identityId, attributes }), and replace principal.licenses with principal.plan in your policies and role documents. If what the attribute described is really a product seat, model it with the licenses domain instead and keep using principal.licenses.

Server-owned context keys

What changed. Values for principal.guest, principal.guestSponsorId, principal.homeTenantId, and principal.licenses that your resolveContext callback or a plugin returns are removed before the server sets its own, the same treatment every other principal.* key already had.

Who is affected. An application that supplied one of these keys itself. Its value is silently dropped, so policies that tested it see the server's value (for example principal.licenses is [] for a person without a seat).

iam.ts
import { betterIam } from 'better-iam';
import { billing } from './billing';

export const iam = betterIam({
  // ...database, secret, baseURL
  async resolveContext(principal) {
    const account = await billing.accountFor(principal.session.tenantId);
    return { 'principal.licenses': account.products };
  },
});

Then change the conditions that read the old key:

policy.json
{
  "effect": "allow",
  "actions": ["reports:export"],
  "resources": ["*"],
  "conditions": { "ArrayContains": { "app.products": "analytics" } }
}

See Application keys for how resolveContext keys reach policies.

Resending an invitation checks it first

What changed. identities.resendInvitation now sends only an invitation that could still be accepted. When the inviter can no longer grant a role or group the invitation carries (for example their grant authority was revoked, or the group now holds license seats they may not assign), the call fails with INVITATION_INVALID and the message ends with revoke this invitation and invite the person again. In 0.2 it sent a fresh token that acceptance then refused.

Who is affected. Applications and admin screens that resend member invitations. Handle the new error by revoking the old invitation and creating a new one under a caller who can grant what it contains.

scripts/resend-invitation.ts
import { IamError } from 'better-iam';
import { iam } from './iam';

export async function resend(
  credential: { token: string },
  tenantId: string,
  invitation: { id: string; email: string; roleIds?: string[]; groupIds?: string[] },
) {
  try {
    await iam.api.identities.resendInvitation(credential, { tenantId, invitationId: invitation.id });
  } catch (error) {
    if (!(error instanceof IamError) || error.code !== 'INVITATION_INVALID') throw error;
    await iam.api.identities.revokeInvitation(credential, { tenantId, invitationId: invitation.id });
    await iam.api.identities.invite(credential, {
      tenantId,
      email: invitation.email,
      roleIds: invitation.roleIds,
      groupIds: invitation.groupIds,
    });
  }
}

Route: POST /identities/resendInvitation, POST /identities/revokeInvitation, POST /identities/invite

Behavior changes for the new features

These changes only take effect once you use guests or licenses, but they change what existing rules and calls do, so review them before you invite the first guest or assign the first product.

Rules and guests

Birthright package rules, onboarding scopes, and workflow scopes gain the key identity.guest. A rule that never tests it no longer matches guests, so guests do not receive the default access you give members. To give guests access through a rule, write a rule that tests identity.guest explicitly. See Birthright rules and guests.

Licensed groups need iam:licenses:assign

Once a license product is assigned to a group, every call that adds people to that group claims seats, and also needs iam:licenses:assign. Without it the call fails with ACCESS_DENIED and nothing is saved. This covers:

  • identities.createMany and identities.invite with groupIds (re-checked at acceptance and by identities.resendInvitation);
  • groups.addMember, groups.addMembers, and groups.updateMember;
  • guest invitations, guests.convertToMember with clearExpiry, access packages, onboarding completion groups, team administrators, and configuration sync.

Team maintainers, directory sync (SCIM), and guest renewals stay exempt. Grant iam:licenses:assign on iam/licenses/assignments to the administrators and service accounts that onboard people into those groups; see Licenses.

Other changes you may notice

ChangeDetail
threats.rules lists 22 rulesThe new classified-access-attempts rule counts clearance refusals. It is on by default and stays silent without the clearances option, because nothing produces clearance refusals.
threats.listIncidents takes a list of statusesstatus: ['open', 'investigating'] lists every active incident in one call. A single status still works.
threats.timeline takes untilBounds the window; since defaults to seven days before it.
Threat views carry the subject's kindDetections and incidents name identity subjects with user, service, or agent.
threats.reportSuspicious and threats.configure name people as the rules doCarol (carol@acme.test) rather than Carol. Update code that parsed the old text.

Only with the clearances option

Security clearances are off unless the deployment sets clearances. Without it, nothing below applies and the clearances API group answers FEATURE_DISABLED (403). If you turn it on during or after the upgrade:

  • The option is an object. clearances: {} or clearances: { appliesToRoot: false }. clearances: true fails construction with INVALID_CONFIG (clearances must be an object), as does any unknown setting.
  • More reserved names. clearanceLevel, clearanceRank, clearanceStatus, clearanceCompartments, and clearanceCitizenship become reserved principal names (INVALID_CONFIG at startup), and classification, classificationRank, compartments, noforn, and releasableTo become reserved resource attribute names (INVALID_CONFIG for types in your configuration, INVALID_INPUT for tenant-registered types). Values under those names from resolveContext or a plugin are removed.
  • Root administrators no longer read up. With appliesToRoot left at its default (true), a root administrator is refused with CLEARANCE_REQUIRED (ACCESS_DENIED to callers) on every labeled application resource above its own clearance, in authorize, listAccessible, and data filter plans. Every iam:* action keeps the root override. Set appliesToRoot: false to keep the 0.2 behavior.
  • Plugin endpoints on iam/{type}/{id} records. In a tenant whose scheme requires labels, a plugin endpoint with its own (non-iam:) action on a record of a required type is refused until that record is labeled, or until the scheme sets a defaultLabel.
  • "View as" refusals. A refusal caused by the impersonating administrator's own clearance is now CLEARANCE_REQUIRED instead of IMPERSONATOR_DENIED.
iam.ts
import { betterIam } from 'better-iam';

export const iam = betterIam({
  // ...database, secret, baseURL, permissions, resolveResource
});

See Security clearances for defining a scheme, granting clearances, and labeling resources.

From 0.3.0 to 0.3.1

Version 0.3.1 changes only the security clearances module. If you never set clearances, upgrade with the same steps and nothing else changes. If you enabled clearances on 0.3.0, upgrade now: 0.3.0 let some labels stop applying. The fixes refuse things 0.3.0 allowed:

Call or rule0.3.00.3.1
tenants.reparent into a subtree with another schemeAllowed; labels in the moved tenant stopped applyingRefused with RESOURCE_IN_USE while the subtree holds labels or live (interim, active, suspended) clearances
A label in a tenant that no scheme applies toIgnoredRefuses everyone, root included
resources.delete of a resource with an inherited labelDropped the inherited labelKeeps it as the resource's own label (audited as classification:label with reason: 'resource-deleted')
Label types containing /AcceptedINVALID_INPUT: label types must be resource type names
updateScheme re-ranking a levelAllowed for levels not in useNever allowed; a removed level comes back only at its former rank (RESOURCE_IN_USE)
requireLabels: ['*']Also covered the built-in model-tool type, which refused every inference:use-tool decisionCovers application types only
Labels on model/{name}Applied only in the tenant that set themApply in every tenant below too
Labels on ssh-host/{host}Followed inheritToChildrenApply to every login of the host
Verifiable credential offers (iam:vc:issue)Did not check the holder's clearanceNeed the holder to dominate the credential type's label at offer, redemption, and every verifiableCredentials.sweep()

After upgrading a deployment that uses clearances and issues verifiable credentials, run iam.verifiableCredentials.sweep() once (the hourly job does it too), so credentials whose holder no longer dominates the credential type's label are revoked.

Verify it works

  1. npx better-iam migrate --config better-iam.config.mjs prints Database and plugin migrations applied.
  2. npx better-iam doctor --config better-iam.config.mjs reports no errors, and iam.selfCheck() returns no findings you did not expect.
  3. Your application starts without INVALID_CONFIG.
  4. A person who signed in before the upgrade still reaches a protected page, which confirms their session carried over.
  5. If you renamed a context key or attribute, check one person with policies.effectiveActions and confirm the actions that depend on the renamed key are still allowed.

Troubleshooting

Next steps

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page