Follow the account contract

Arc CMS owns accounts, sign-in, roles and payments. Your app, built on a copy of Arc CMS or running elsewhere against the same Firebase project, relies on the few rules on this page and keeps to its side of them.

One person, one account

A person is one Firebase Auth account plus one document in the users collection. The document id is not the Auth uid: the document holds the uid in its uid field. Roles and the admin screens for them are in Users and roles; how the role reaches the token is in Roles and claims.

You needUse
The signed-in person's record idThe arccms_uid claim in their ID token
Their recordusers/{arccms_uid}, readable by its owner and by admins
Their Arc CMS roleThe arccms_role claim (isAdmin() and isEditor() in rules)
Their planThe premium fields on the record: Add payments and premium features
Their private datausers/{userDocId}/... in Firestore and in Storage
Clean up data kept elsewhereA trigger on users/{docId} deletes, or the user.deleted event
To know when a sign-in was deleted but its record was notThe user.signInDeleted event (below)
An account for someone with no email and no phonecreateAppAccount() from the app kit: App accounts. Its record has email and phone set to '' and by: 'app', and the person cannot change or delete it (Locked by default)

Read the record id from the token

Every account carries the custom claim arccms_uid, whose value is its users document id. Read it from the ID token instead of querying:

const token = await auth.currentUser.getIdTokenResult();
const userDocId = token.claims['arccms_uid'] as string; // users/{userDocId}

In security rules, ownsUserRecord(userDocId) compares the claim with no document read.

When the claim is set

A token without the claim owns nothing. ownsUserRecord() is false, so rules deny. Treat a missing claim the same way in app code.

Keep per-person data under the record

WherePathWho can use it
Firestoreusers/{userDocId}/{collection}/..., at any depthNobody but the Admin SDK until your app grants access in firestore.app.rules
Storageusers/{userDocId}/..., or under the install's upload folder, such as arccms/users/{userDocId}/...The owner and admins can read; your app grants writes, with its own size and type limits, in storage.app.rules. Never public.

A minimal Firestore grant:

// firestore.app.rules
match /users/{userDocId}/progress/{lessonId} {
  allow read, write: if ownsUserRecord(userDocId);
  allow read: if isAdmin();
}

Writing and testing these files is in Set security rules and indexes.

What deleting an account removes

An admin deletes an account under Users, or the person deletes their own under Profile. Deleting your own account needs a sign-in within the last 10 minutes, and an admin's account can only be removed by another admin. Either way the users document is deleted, and then the server:

  1. Ends access first: removes the Arc CMS claims and ends every session, so a failure later never leaves a removed admin an admin.

  2. Firestore: deletes every subcollection under users/{userDocId} at any depth, the person's in-app notifications (Notifications) and their feedback (Feedback).

  3. Storage: deletes the folder users/{userDocId}/ (under the upload folder, if the install has one) and the profile photos in avatars/{uid}/.

  4. Sign-in: deletes the Firebase Auth account, the email lookup entry, the phone number index entry and the PIN, and your app's PINs and its gates' counts of wrong phone PINs (see PINs for your app). The Auth account is kept when the record's authOwner is host or shared (another app owns or shares that login); it then only loses the Arc CMS claims.

  5. Event: emits user.deleted on the event bus (AppEvents), with userId (the Auth uid) and data.userDocId, once every step above has worked.

A failed step makes the whole cleanup run again, for up to an hour after the delete. Every step is safe to repeat.

The person's contact: deleting your own account also erases the contact for your address (its lists, consent and form sign-ups), with a receipt in ErasureLog that holds no address. An admin deleting a user only unlinks the contact, since the address may have been on a list before the account existed.

When a sign-in is deleted some other way

Arc CMS deletes the users record first and the sign-in after it. A sign-in can also be deleted without Arc CMS: by the person's browser (auth.currentUser.delete(), which Firebase allows any signed-in account) or by another app sharing the Firebase project. Arc CMS then keeps the record, deletes nothing and emits user.signInDeleted, with userId (the Auth uid), data.userDocId, and data.locked (whether it is a locked app account). user.deleted is not emitted, because the account is not deleted. The event fires once per deletion, and never for Arc CMS's own deletions.

What to do is your app's choice. For an app account, give the sign-in back with restoreAppSignIn() or delete the account with deleteAppAccount() (see App accounts). For anyone else, an admin can delete the record under Users.

Data your app keeps elsewhere

Anything outside the record (another collection, another service) is yours to delete. The simplest way is a trigger of your own on the same delete, in functions/src/custom/index.ts:

import { onDocumentDeleted } from 'firebase-functions/v2/firestore';
import { db } from '../init.js';
import { arcDocument } from '../arc-config.js';

export const onUserRecordDeleted = onDocumentDeleted(arcDocument('users/{docId}'), async (event) => {
    const snap = await db.collection('Orders').where('userDocId', '==', event.params.docId).get();
    await Promise.all(snap.docs.map((d) => d.ref.delete()));
});

It deploys as arccms-custom-onUserRecordDeleted and may run more than once, so keep it safe to repeat. See Write your own Cloud Functions. To act only after Arc CMS's own cleanup has finished, react to the user.deleted event instead.

What your app may and may not write

Your app mayYour app must not
Write its own data under users/{userDocId}/, with its own rulesCreate users documents from the browser. Only admins may; every sign-up creates its record on the server.
Let the person edit their own record's other fieldsWrite role, uid, isActive, status, authOwner, email, phone, phoneVerified, or any premium or credit field. The rules refuse these from the owner; the server writes them.
Give its own users a plain role claim. Arc CMS never reads or writes role, so it grants no Arc CMS access.Name a claim arccms_ anything
Set claims of its own with mergeAppClaims() from the app kit, which merges them with the existing ones (see App accounts)Replace all claims. Arc CMS always merges; setCustomUserClaims with only your keys wipes arccms_role and arccms_uid.

An app that sets claims with setCustomUserClaims(uid, { plan: 'pro' }) removes the Arc CMS claims from that account: its rules then deny the person their own record, and an admin loses the admin area until the Arc CMS app next loads and puts the claims back. Use mergeAppClaims() instead: it merges, and takes turns with Arc CMS's own claims writes on the same account. Reading the claims yourself and writing them all back keeps Arc CMS's claims, but can still lose a write Arc CMS makes at the same moment.

An app outside this repository

An app or mobile client in the same Firebase project signs people in with the same Firebase Auth, then reads arccms_uid as above. If the claim is missing, it calls arccms-refreshMyClaims with no input and refreshes the token; the reply is { arccms_uid }, or an error when the login has no Arc CMS record or is blocked. When Arc CMS runs in its own named database, read the record from that database. Sign-in from an outside client is in Set up sign-in.