Firestore Profiles
Standalone WordPress profiles with secure, optional Firestore activity
by Dexter Adams · github.com/dexter-adams/firestore-profiles · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/dexter-adams/firestore-profiles/archive/refs/heads/main.zipReadme
Firestore Profiles
A standalone WordPress profile plugin with optional, server-side Firestore activity. It provides public member routes, editable WordPress-native profiles, comments, badges, privacy preferences, and paginated quiz/poll activity without requiring a companion theme, ACF, a Firebase SDK, or another plugin.
About this code sample
What I wrote. Version 2.0 is mine end to end. It is a ground-up rewrite of a profile system I had built and run in production, where the original had grown to roughly 5,000 lines that only functioned inside one specific site — it called that project's Firebase wrapper class, its quiz and poll classes, its ACF field definitions, its theme CSS, and its block template parts. This version keeps the behavior and discards every one of those couplings.
Why I wrote it. The original worked, and that was the problem: none of it could move, and no part of it could be reasoned about alone. Rewriting it forced the question I actually wanted answered — which of these responsibilities belong to WordPress, and which genuinely belong to Firestore? The answer turned out to be lopsided. WordPress already owns identity, profile fields, uploads, permissions, comments, routing, and templating; it just was not being allowed to. Firestore's real job was much smaller: read-only activity records. Once that line was drawn, Firestore became optional. With no credentials configured, profiles, editing, comments, badges, and privacy all still work, and the activity pages show a neutral setup message instead of breaking.
Why I am proud of it. Two decisions. First, the credentials never leave the server: the service-account JSON lives outside the web root, the plugin signs its own JWT and exchanges it for a short-lived OAuth token held only in memory, and neither the key nor the token is ever written to the database or sent to a browser. Second, Firestore documents do not reach templates. They are reduced to an explicit allowlist — title, result, timestamp, an optional HTTP(S) URL — so a schema change upstream, or a field somebody adds later without thinking about this plugin, cannot publish internal IDs or email addresses onto a public profile page. An allowlist fails closed; a denylist fails open, and on a public route that difference is the whole thing.
What it demonstrates. Third-party API integration done defensively —
service-account JWT/OAuth handled server-side, network timeouts, redirect and
response-size limits, validated query identifiers, and bounded pagination. It
also demonstrates dependency inversion applied to a real legacy problem: badges
now arrive through a documented generic action, so quiz and poll systems
integrate with this plugin without it knowing they exist. Legacy kbs_*
metadata is still read so existing members migrate cleanly, but no legacy class,
function, plugin, or theme is ever called.
Why this version is standalone
Version 2.0 is a ground-up extraction of production profile behavior into explicit boundaries:
- WordPress owns identity, profile fields, uploads, permissions, comments, and routes.
- A small REST client owns read-only Firestore access.
- Activity records are normalized through an allowlist before templates receive them.
- Quiz and poll products integrate through documented filters and actions.
- Plugin templates and responsive styles work with any active theme. Theme overrides are optional.
- Legacy metadata is read for a smooth migration, but no legacy class or function is called.
Requirements
- WordPress 6.0 or later
- PHP 7.4 or later with OpenSSL
- Pretty permalinks
- Optional: a Google Cloud service account with read access to the selected Firestore collections
Installation
- Copy this directory to
wp-content/plugins/firestore-profiles. - Activate Firestore Profiles.
- Profile routes register on activation, and re-register themselves on the
first request after the plugin version changes, so an update that replaces
the files in place cannot leave stale rewrite rules behind. If
/u/{username}/still does not resolve, visit Settings → Permalinks and save once. - Set a user's Firestore identity when activity should be shown:
update_user_meta( $user_id, 'firestore_profiles_uid', $firestore_uid );
Profile URLs use /u/{user-nicename}/ by default. The base can be changed:
add_filter(
'firestore_profiles_route_base',
static function (): string {
return 'members';
}
);
Secure Firestore configuration
Firestore is optional. Without credentials, the WordPress profile, comments, badge, and editing features continue to work and the activity pages show a neutral setup message.
Store the service-account JSON outside the web root and outside this repository, restrict its file permissions, and add this to wp-config.php:
define( 'FIRESTORE_PROFILES_SERVICE_ACCOUNT', '/secure/path/firestore-reader.json' );
The project ID is read from that file. To override it:
define( 'FIRESTORE_PROFILES_PROJECT_ID', 'your-google-cloud-project' );
Grant the service account only the Firestore read permission it needs. Never commit the JSON key. The client creates a short-lived OAuth token in memory; neither the key nor the token is sent to the browser or saved in WordPress.
Default activity sources:
| Section | Collection | User field | Order field |
|---|---|---|---|
| Quizzes | userQuizzes |
userId |
timestamp |
| Polls | pollVotes |
userId |
timestamp |
Firestore may request a composite index for the equality filter plus timestamp ordering. Follow the index link in the server-side Google error, or change the mapping:
add_filter(
'firestore_profiles_activity_sources',
static function ( array $sources ): array {
$sources['quizzes'] = [
'collection' => 'quizAttempts',
'user_field' => 'profileUid',
'order_by' => 'completedAt',
'direction' => 'DESCENDING',
];
return $sources;
}
);
Collection and field names are validated before a request is sent. Queries are bounded and response bodies are size-limited.
Data shown publicly
Firestore documents are not passed directly to templates. The activity repository exposes only:
- title
- result or selected choice
- timestamp
- an optional HTTP(S) URL
It recognizes a short list of common field aliases in ActivityRepository::normalize(). Adapt that method or its upstream source if your schema differs. Do not expose email addresses, tokens, internal IDs, or unrestricted document maps on a public profile.
Only approved WordPress comments are displayed. User email addresses are never included in public template data. Public profile routes emit noindex,nofollow by default. Sites that intentionally want indexable profiles can return false from the firestore_profiles_noindex filter.
Badge integration
Approved WordPress comments update comment badges automatically. An independent quiz, poll, import, or scheduled job can report a trusted total:
do_action( 'firestore_profiles_activity_total', $user_id, 'quiz', $completed_quiz_count );
do_action( 'firestore_profiles_activity_total', $user_id, 'poll', $poll_vote_count );
Use firestore_profiles_badge_definitions to replace or extend the badge catalog. Badge state is stored in firestore_profiles_badges.
Templates
The plugin renders its own accessible document layout. To customize a view, copy one of the files from templates/ to:
your-theme/firestore-profiles/{template-name}.php
Available names are layout, profile, activity, comments, badges, edit, preferences, and not-found. Overrides are optional and filenames are validated.
Privacy and preferences
Profiles are public by default for compatibility. A member or authorized administrator can make a profile private from the edit screen. Private profiles are visible only to the member and users with permission to edit that account.
Preferences are storage and integration points; this plugin does not send email by itself.
Development
No build step or Composer install is required.
find . -name '*.php' -not -path './.git/*' -print0 | xargs -0 -n1 php -l
php tests/run.php
CI runs the same checks on PHP 7.4, 8.1, and 8.3.
Uninstalling
Deactivation keeps all profile data. Uninstall also keeps data by default. To opt into deletion, define the following before deleting the plugin:
define( 'FIRESTORE_PROFILES_DELETE_DATA', true );
The uninstall routine removes only metadata owned by version 2; it never removes user accounts, media, comments, Firestore records, or legacy metadata.
License
GPL-2.0-or-later. See LICENSE.txt.