Guide

User Accounts architecture

includes/class-plugin.php boots REST, user data, Firebase-backed stores, page/form provisioners, permalink helpers, account firewall, patterns, and blocks under src/.

Authentication model

  • Firebase Authentication handles sign-in (magic link and federated providers).
  • REST mutations authenticate with X-PRC-User-Id and X-PRC-User-Token headers (see extract_user_auth_from_request()).
  • POST /user-accounts/get-user additionally requires a WordPress nonce (prc-user-accounts) in the JSON body before Firebase work runs.

Provisioning

ClassRole
Page_ProvisionerEnsures /my-account/* pages exist
Form_ProvisionerSeeds login, verify-token, and complete-profile forms
PatternsBlock patterns for buttons and forms

Older rewrite maps under class-permalinks-rewrites.php still describe /profile/* paths; current page provisioning and client redirects use /my-account/*. Prefer /my-account/ for new work.

Protected files

Attachment meta _prc_protected is the source of truth. Protected_Files::grant_for_user() returns a file URL only after the caller has constructed User_Data (token verified). The Private Files block stores display IDs only; it never prints file_url. Dataset downloads call the same grant, then keep dataset telemetry and users/{uid}/datasets.v2. Generic downloads write users/{uid}/files.v1 through log_file_downloads() (one unpathed multi-location update). When the generic grant omits dataset_id, datasets still run ATP if the attachment belongs to an ATP dataset.

Private Files download analytics live on the host post, keyed by each block's persisted instanceId. Private_Files::record_download() writes _private_files_blocks and yearly nested maps so two Private Files blocks on one post (including a dataset post) keep separate totals. Recording runs after a successful grant only when post_id and instance_id are present and the attachment is listed on that instance. A failed record does not fail the grant. Gutenberg clientId is not used. Two copies of the same synced pattern on one post share one instanceId and therefore one bucket. Until a block is saved with an instanceId, downloads are not recorded. A post that already has legacy _private_files_total_downloads and exactly one Private Files block copies those maps into that instance once.

Protected_Files::zip_for_user() is the zip operation: grant every ID, fail closed on any denial, stage bytes with wp_remote_get into get_temp_dir(), then stream a Protected_Files_Archive. Stars live on a sibling RTDB map users/{uid}/starredFiles/{id} (star / unstar / get_starred); a bookmark is not a grant.

VIP cache and firewall

Account_Firewall integrates with VIP vary-cache so authenticated and anonymous readers can receive different HTML without breaking edge cache. Cookies register with the WP Consent API as functional cookies.

Security touchpoints

ControlWhere
Firebase token validationUser_Data::check_token() on protected-file download and get-user
Protected file grantProtected_Files::grant_for_user()
WP nonce on get-userpermission_check_get_user
Cloudflare TurnstileAuth forms when PRC_PLATFORM_TURNSTILE_SECRET_KEY is set and a captcha token is submitted
Account deletionDELETE /user-accounts/delete-user

Required configuration

ConstantPurpose
PRC_PLATFORM_FIREBASE_CONFIGClient Firebase config array
PRC_PLATFORM_TURNSTILE_SECRET_KEYOptional Turnstile verification
DEFAULT_TECHNICAL_CONTACTTechnical contact for notifications

Do not commit service-account JSON or real secrets into docs/.

Was this helpful?