Data model

This page lists every Firestore collection and Storage path Arc CMS uses, who can read and write each one, and which feature owns it.

Where the data lives

Arc CMS keeps all its data in one Firestore database and one Storage bucket per install.

How content is stored

Content has no single collection. Each content type gets three, named from the type's slug: arc_{slug}_drafts for what the editor saves, arc_{slug} for the published copy (same document id as the draft), and Tags_{slug} for its tags. Each type has a slug of its own, which the edit form locks once any of the three holds a document (data import refuses to change it then too), and deleting a content type deletes all three with every subcollection under their documents, unless another type still uses the slug (possible only on a site set up before slugs were checked). See Content below.

Firestore rules cannot match a prefix, so firestore.rules has one wildcard rule, match /{collection}/{docId}, that checks the collection name with a regular expression.

The wildcard rule applies to any collection whose name starts with arc_, Tags_ or WaitlistUserTags_. An app collection named arc_orders becomes public to read. Give app collections names without these prefixes.

Your app's collections

An app keeps its own rules in firestore.app.rules and storage.app.rules. npm run rules:build puts them into the core rules at the // @arc-app-rules line, so they can call the core helpers. See Set security rules and indexes. For how the rules are built and tested, see Security rules.

Who can read and write

The rules use a few helpers. The role comes from the arccms_role claim, never a plain role; see Roles and claims.

In the tables below, "Server only" means no browser can read or write it: only Cloud Functions do, through the Admin SDK, which bypasses the rules. Every collection with no rule is server only too, because Firestore denies what no rule allows.

Content

CollectionHoldsReadWriteFeatureKey fields
ContentTypesOne content type definition per document, each with its own slug. Deleting one deletes its arc_{slug}, arc_{slug}_drafts and Tags_{slug} collections, unless another type still uses the slug.Public (SSR routes with it)StaffContentname, singularName, slug, fields, templateFolder, hasPublicUrl, entryOrder (newest or manual), standard (pages on the built-in Info Pages type), searchFields, nameTranslations
arc_{slug}_draftsThe type's items as the editor saves them. Subcollection translations/{lang} holds only the fields that differ in that language.StaffStaffContenttitle, content, urlSlug, status, summary, tags, seoTitle, metaDescription, authorId, customFields, lastPublishedAt, sortOrder (the place in a type kept in its own order), layout (the entry's detail layout, detail-{layout}.html; empty for detail.html)
arc_{slug}Published items, same id as the draft. Subcollections: translations/{lang} (public read, written by the publish pipeline) and PublishedHistory (a copy per publish, admin read, server write) and DeploymentLogs (the steps of each deploy to Hosting, server write).PublicStaff; in practice the publish pipelineContentThe draft's fields, plus publishedOn, deployStatus, deployedAt, deployedUrl, deployError
Tags_{slug}The tags of one content type.PublicStaffAuthors and tagslabel, color, contentTypeSlug, usageCount
AuthorsBylines. No email and no user id, since it is public.PublicAdminAuthors and tagsname, slug, bio, photoUrl, jobTitle, url, sameAs
mediaOne document per image in the media library. The files are in Storage.StaffStaffMediadownloadURL, name, title, alt, notes, uploadTime, width, height, variants, source
mediaFilesOne document per document in the media library's Files (PDF, Word, Excel, PowerPoint, CSV, text). The files are in Storage.StaffStaffMediatitle, notes, name, downloadURL, path, type, contentType, bytes, pages, thumbnail, uploadTime
_publish_queueA request to publish, unpublish, update, delete or redeploy. processPublishQueue runs it and deletes the document.NobodyStaffContentaction, contentTypeSlug, docId, timestamp
_search_queueOne entry per draft save, id {collection}~{docId}. onSearchQueued indexes the draft and deletes the entry.NobodyStaff, only these fields, and collection must be a drafts collectionSearchcollection, docId, at
SearchIndexOne entry per document and language, id {source}:{collection}:{docId}:{lang}. Read only through the search callable, which enforces scope.Server onlyServer onlySearchsource, scope, lang, collection, docId, tokens, fields, title, snippet, link, indexedAt
DraftContentsLegacy. Content lives in arc_{slug}_drafts.AdminAdminContent
ContentsLegacy. Content lives in arc_{slug}.AdminAdminContent

Settings

All site settings are documents in one collection. Most are admin only because several hold secrets.

CollectionHoldsReadWriteFeatureKey documents
SettingsOne document per settings area.Public: email_status, site-usage, misc, cache, global-message, users, localization, onboarding_status, about, feedback. Signed in: analytics_status. Admin: every other document.Admin, for every documentSeveralemail, integrations and analytics hold provider secrets. dodo-payments holds payment keys. users holds defaultRole for new sign-ups. feedback holds enabled. discoverability, search_status, search_collections, contact_fields, sms, app_audience.

Never put a secret in a public Settings document. The documents listed as public are readable without signing in.

Users and sign-in

Every sign-up creates its users record on the server. What an app keeps per person, and what deleting an account removes, is in Follow the account contract.

CollectionHoldsReadWriteFeatureKey fields
usersOne record per person, with an auto id. The id is the arccms_uid claim. Deleting it runs onUserDeleted, which removes the person's subcollections, Storage folders, feedback and notifications, and their sign-in account unless another app owns it.Admin, or the owner (isOwnUserDoc())Create and delete: admin. Update: admin, or the owner for any field except role, uid, isActive, status, authOwner, by, selfService, email, phone, phoneVerified, detachedAt, detachedReason and the premium and credit fields; emailVerified may only go to false. A locked app account also never changes its name or photo. Never a password field.Users and rolesuid, name, email, emailVerified, phone, role, status, isActive, by, selfService (app accounts), authOwner, photo, premium fields such as premiumType and creditBalance
email_lookupOne document per registered email, id the SHA-256 of the trimmed, lowercased address, so the sign-up page can check an address without seeing any.PublicAdmin; mostly the serverSign-in methodsexists
_systemServer secrets and markers: first_admin (written once by claimFirstAdmin), pin_pepper, unsubscribe_secret, sign_in_setup_alert (when the admins were last told phone sign-in needs Google Cloud setup).Server onlyServer onlyUsers and roles
signup_otpsEmail codes for sign-up and for adding an email, id the email hash. Only the code's hash is stored.Server onlyServer onlySign-in methodspurpose, codeHash, expiresAt, attempts, verified
phone_otpsSMS codes, id the hash of the number.Server onlyServer onlySMSpurpose, uid, codeHash, expiresAt, attempts, verified
phone_indexWhich account a phone number belongs to, id the hash of the number.Server onlyServer onlySMSuserDocId, uid, createdAt
auth_pinsThe hashed PIN for phone sign-in, id the Auth uid.Server onlyServer onlySMSsalt, hash, failedAttempts
app_pinsAn app's own hashed PINs, id <namespace>__<uid>. Removed with the account.Server onlyServer onlyPINs for your appuid, namespace, salt, hash, failedAttempts
app_phone_pin_triesWrong phone PINs at an app's gate, id <namespace>__<uid>. Gone after a correct PIN, and removed with the account.Server onlyServer onlyPINs for your appuid, namespace, pinId, failedAttempts
_rate_limitsCounters that slow down repeated requests (codes, PIN tries, feedback alerts, install events, contact messages and their alerts), one per key.Server onlyServer onlySign-in methodswindowStart, count, expiresAt
_claim_locksA short lease, one per account, held while its custom claims are written, so two claims writes on one account take turns. Deleted when the write ends.Server onlyServer onlyRoles and claimstoken, expiresAt
account_transfersA record each time an email or phone number moves from one account to another.AdminServer onlySign-in methodskind, shown, fromUserDocId, toUserDocId, detached, at

Core has no rule for subcollections of users, so the browser cannot touch them until an app grants access in firestore.app.rules.

Payments

CollectionHoldsReadWriteFeatureKey fields
ProductsWhat can be bought, for the pricing page.PublicAdminPaymentsname, active, type, price, currency, premiumType, tierRank, interval, trialDays, creditsGranted, tiers, providerProductIds, purchaseCount
TransactionsOne document per payment event, kept once by idempotencyKey.Admin, or the buyer (userId is their Auth uid)Server onlyPaymentsuserId, productId, amount, currency, status, type, eventType, idempotencyKey, createdAt
WebhookEventsEvery raw webhook from the payment provider, id the webhook-id header. handlePaymentEvent runs on each new one.AdminServer onlyPaymentseventType, rawPayload, signatureValid, processed, receivedAt
CreditLedgerEvery change to a person's prepaid credits, append only.Admin, or the owner (userId)Server onlyPaymentsuserId, delta, reason, balanceAfter, createdAt

Signup forms

In the code and the collection names, a form is a waitlist. Members join through the joinForm callable.

CollectionHoldsReadWriteFeatureKey fields
WaitlistsOne document per form, usually with the form's slug as id. Subcollections: users (one per member, created by joinForm) and users/{id}/notes (admin notes). Older installs may still hold users/{id}/referrals, closed to every client until npm run forms:remove-referrals deletes it.Form: public. Members and notes: admin; signing up goes through callables.Admin. Signups and verification go through the form callables (joinForm, finalizeFormSignup).Signup formsForm: name, slug, isActive, otpEnabled, totalSignups, fields, targetListIds, defaultTagId. Member: email, firstName, emailVerified, isConfirmed, isSubscribed, tags.
WaitlistedUsersLegacy registry of signups across all forms. Arc CMS creates no new records here. Its old referrals subcollection is closed to every client until npm run forms:remove-referrals deletes it.AdminAdminSignup forms
WaitlistUserTags_{formId}The member tags of one form.AdminAdminSignup formslabel, color, waitlistId, usageCount

Email and SMS

CollectionHoldsReadWriteFeatureKey fields
EmailTemplateOne email template per document: the defaults, per-form templates and templates made in the composer.AdminAdminEmail enginetype, category, scope, waitlistId, subject, previewText, template, senderName, senderEmail, isActive, editorVersion
EmailLogsOne document per outgoing email: the send queue and the record of what happened. Created by queueEmail(); onEmailLogCreate sends it.AdminAdminEmail enginetoEmail, subject, template, type, category, status, attempts, createdAt, sendingTime, isOpened
EmailLogLegacy. No code uses it; email records are in EmailLogs.AdminAdminEmail engine
_email_countersSends per provider per day and per hour, for the provider's limits. Id such as {provider}_daily_{date}.Server onlyServer onlyEmail enginecount, provider, type
SuppressionAddresses that must get no marketing email, id the email hash.AdminServer onlyEmail engineemail, emailHash, reason, at
BroadcastEmailsOne document per broadcast. processBroadcast runs when it is created.AdminAdminEmail marketingsubject, template, audience, status, scheduledAt, totalCount, sentCount, failedCount
_broadcast_continueA short-lived document. Creating one runs continueBroadcast, which resumes a long broadcast and deletes it.Server onlyServer onlyEmail marketingbroadcastId, triggeredAt
SmsLogsEvery SMS sent, or only recorded with the log provider.AdminServer onlySMSto, purpose, provider, status, error, createdAt

Audience and automations

CollectionHoldsReadWriteFeatureKey fields
ContactsOne person in the audience, id the email hash. Filled from users, form members, imports and payments.AdminServer only; admins change contacts through callablesAudienceemail, emailHash, name, sources, userId, listIds, tags, fields, consent, disabled
ListsAudience lists: system lists such as all-users, one list per form, and lists admins make.AdminAdminAudiencename, description, type, formId, conditions, memberCount
ContactTagsTags on contacts, id a slug of the label. Not the content tags in Tags_{slug}.AdminAdminAudiencelabel, color, usageCount
ErasureLogProof that a contact was erased, id the email hash. It holds no address.AdminServer onlyAudienceemailHash, erasedAt, erasedByUid, removed
DripCampaignsOne drip sequence: the list it follows and its steps.AdminAdminEmail marketingname, listId, status, trigger, steps, exit, counts
DripEnrollmentsOne contact's progress through one sequence, id {campaignId}_{contactId}.AdminServer onlyEmail marketingcampaignId, contactId, status, currentStep, nextSendAt
AnnouncementsOne announcement sent by the sendAnnouncement callable.AdminServer onlyEmail marketingtitle, body, link, audience, sendEmail, status, counts
AppEventsThe event bus: one event per document, such as user.signed_up or payment.succeeded. onAppEventCreate runs the matching actions.AdminServer onlyAutomationstype, userId, contactEmail, data, processed, createdAt
NotificationsOne in-app notification for one person.Admin, or the person (userId is their Auth uid)The person may change only read and readAt. Create and delete: server only.NotificationsuserId, type, title, body, link, read, createdAt

Other collections

CollectionHoldsReadWriteFeatureKey fields
FeedbackOne item from the feedback button. onFeedbackCreated adds sender.AdminCreate: a signed-in person, for themselves, while the button is on, with status new, known fields and short values, and files only from their own feedback folder. Update and delete: admin.Feedback buttonuid, userDocId, message, screenshotPath, voicePath, voiceSeconds, page, device, status, createdAt
ContactMessagesOne message from a contact form on the site. Only submitContactMessage writes it.AdminUpdate (status only: new, done, spam) and delete: admin. Never written from a browser.Contact form (contact)name, email, phone, subject, message, page, lang, status, createdAt
PwaStatsInstall prompt counts per day, id YYYY-MM-DD, written by trackPwaEvent.AdminServer onlyInstallable app (PWA)date, prompt_shown, installed, dismissed, opened_installed
AnalyticsDashboardsOne document, analyticsData: the Google Analytics numbers the dashboard shows.AdminAdmin; filled by the analytics callablesAnalyticsmetrics, acquisitionPanels, dateRange, lastSyncDate, propertyId

Server-only collections with no rule

These have no rule in firestore.rules, so only Cloud Functions reach them.

CollectionHoldsFeatureKey fields
form_otpsEmail codes for forms that verify members, id {formId}_{emailHash}.Signup formswaitlistId, emailHash, codeHash, expiresAt, attempts, verified
CountedBuyersA marker per payment or subscription, so a buyer adds to a product's purchaseCount once.PaymentsproductId, countedAt
AppAudienceArc CMS's own state for each of the host app's users, id the SHA-256 of the person's unique key, so no app identifier is stored in the clear.App audienceconsent, consentChangedAt, deleted

Published content items also get a DeploymentLogs subcollection: one record per static page deploy, written by the publish pipeline and never read by the browser.

Storage

Files live in the install's bucket. The browser puts storagePrefix in front of media, library files, imported files and feedback files, so with arccms/ the media library writes arccms/mediaImages/... and arccms/mediaFiles/.... Profile photos in avatars/ always go to the bucket root. The rules accept both forms: {prefix} below is that one folder.

PathReadWriteLimitsWhat writes there
{fileName}PublicStaffNoneAny file at the bucket root.
{first}/{second}/{rest=**}Public, unless the first or second folder is usersStaff, except in a mediaFiles folderNoneThe media library's images (mediaImages/{name}-{size}.{ext}, one file per size), files uploaded on the Data pages, and every other admin upload.
mediaFiles/{fileName} and {prefix}/mediaFiles/{fileName}PublicStaff (isLibraryFile)Only PDF, Word, Excel, PowerPoint, CSV and text, each extension with its own content type, up to 20 MB; a PDF's -thumb.jpg under 1 MB. Delete by staff.The media library's Files (Media).
users/{userDocId}/{rest=**} and {prefix}/users/{userDocId}/{rest=**}The owner (ownsUserRecord) or an adminStaff through the general rule; apps grant owners write in storage.app.rulesSet by the appAn app's per-person files. Deleting the account deletes the folder.
users/{userDocId}/feedback/{feedbackId}/{fileName} and {prefix}/users/{userDocId}/feedback/{feedbackId}/{fileName}The owner or an adminCreate only, by the owner, and only a new filescreenshot.jpg as image/jpeg under 5 MB; voice.mp4, voice.webm or voice.ogg as audio under 10 MBThe feedback button. Admins delete the files with the feedback.
avatars/{uid}/{fileName}PublicThe signed-in person whose Auth uid is {uid}, or staffAn image under 5 MB, or a deleteThe profile page, which stores the photo's URL in the users record's photo. Deleting the account deletes the folder.

Anything outside a users folder is public to read, including files an app uploads elsewhere. Keep private files in users/{userDocId}/ (under the prefix, when there is one), and grant writes there in storage.app.rules.