Recent Feature Updates
This page is a dated, append-only changelog of development cycles: what shipped, how each change works, the key source files, and — importantly — what you need to do to deploy it (database migrations, new environment variables).
Each cycle gets its own ## YYYY-MM-DD — Title section, newest first. When a new cycle lands, add a new dated section above the previous one — never rewrite or delete an earlier section's content. That's what makes this a durable record of what shipped when, instead of a snapshot of "the latest cycle" that a future rewrite silently erases.
2026-07-21 — Full-codebase feature-doc reconciliation
This page summarizes what shipped in this development cycle, how each change works, the key source files, and — importantly — what you need to do to deploy them (database migrations, new environment variables).
This cycle was a full-codebase feature-doc reconciliation: a fleet of agents audited every feature doc against the running code across the whole app — accounts, auth & security, messaging, feed & content, monetization, discovery, and the admin/moderation surface. Along the way several genuinely new capabilities shipped (expanded account moderators with an audit trail, in-chat translation, subscription free-trial/discount offers, PayPal & Apple Pay / Google Pay coin purchases, a platform coin wallet, per-photo free previews on paid posts, and admin passkeys), while many stale, fabricated, or drifted GraphQL signatures were corrected to match reality. 8 new database migrations landed alongside this work, and chat translation introduces new (optional) environment variables.
TL;DR configuration: run the 8 new database migrations after pulling. There are new optional environment variables for chat translation (the feature stays disabled until a translation provider is configured). No other feature in this cycle requires configuration. See Deployment & configuration below.
Highlights — genuinely new this cycle
- Account moderators, expanded — permission set grew from 5 to 9, with a step-up re-auth window and a full action audit trail (migration
20260721150000). - In-chat translation — per-conversation message translation with a provider-agnostic backend (migration
20260721160000, new env vars). - Subscription offers — free-trial / discount links for creators (migration
20260721170000). - More ways to buy coins — Stripe Express Checkout (Apple Pay / Google Pay) and PayPal, plus an admin-managed platform coin wallet (migrations
20260721190000). - Per-photo free previews on paid posts, plus a struck-through original price when a creator discounts an already-paid post (migrations
20260721180000,20260721200000). - Admin passkeys — WebAuthn credentials for admin login (migration
20260721140000). - Logged-out browsing — public Clips grid and public search for unauthenticated visitors, plus a dedicated Discover People page.
Monetization & coins
More ways to buy coins (PayPal, Apple Pay, Google Pay)
- GraphQL: the legacy
completeCoinPurchase/createCoinPurchasemutations are gone. The new flow iscreateCoinPurchaseIntent/confirmCoinPurchaseIntent(Stripe PaymentIntent, backs Apple Pay / Google Pay) andcreatePaypalCoinOrder/capturePaypalCoinOrder(PayPal), returning the newStripeCoinIntent/PaypalCoinOrdertypes. - UI: a new
CoinExpressCheckout.tsx(Stripe Express Checkout Element + PayPal Buttons) renders aboveStripeCheckoutin the coins payment modal. Coin-package selection moved to/settings/get-coins(GetCoinsPage.tsx);/coinsnow redirects there. - Model: a
CoinPurchaselinks to aPaymentTransactionwhoseprovideris a free-form string ('stripe'or'paypal'), not Stripe-only. - The
CoinTransactionTypeenum grew from 6 to 16 values (addstip_sent,tip_received,post_purchase,conversation_subscription,post_sale,subscription_revenue,product_sale,cashout_requested,cashout_reversed,cashout_canceled). - Source:
apps/backend/graphql/types/coin-purchase.type.js,apps/backend/graphql/resolvers/coin-purchase.resolver.js,apps/backend/managers/coin-managers/coin-purchase.manager.js,apps/frontend-nextjs/src/components/payment/CoinExpressCheckout.tsx,apps/frontend-nextjs/src/page-components/CoinsPage.tsx.
Platform coin wallet (admin)
- Model: a single platform-wide coin wallet (
PlatformWalletmodel + migration20260721190000-create-platform-wallet, seeded with oneplatformrow) and its manager. - GraphQL (admin):
adminPlatformWallet,adminGetUserCoinBalancequeries;adminTopUpWallet,adminTransferCoinsToUser,adminTakeCoinsFromUsermutations. - UI:
UserCoinsCard.tsxon the admin user-detail page. - Source:
apps/backend/database/models/PlatformWallet.js,apps/backend/managers/coin-managers/platform-wallet.manager.js,apps/backend/graphql/types/admin/platform-wallet.type.js,apps/backend/graphql/resolvers/admin/platform-wallet.resolver.js,apps/frontend-admin/src/components/UserCoinsCard.tsx. - Note: the non-admin coin-package CRUD mutations were removed from
coin-package.resolver.jsand replaced by admin-namespaced equivalents (adminCreateCoinPackage/adminUpdateCoinPackage/… plusadminGetCoinPackages).
Creator subscriptions & subscription offers
- GraphQL: a brand-new subscription-offer system —
createSubscriptionOffer/updateSubscriptionOffer/deactivateSubscriptionOffer/deleteSubscriptionOffer,myOffers,offerByToken,applicableDiscount,redeemSubscriptionTrial— a free-trial/discount-link mechanism that didn't exist at the last sync (migration20260721170000). - GraphQL: new
subscriberRetention(months)query (SubscriberRetentionPoint) — month-bucketed churn/retention analytics, computed in application code from the creator's fullUserSubscriptionhistory. - UI: a full tier-CRUD screen now exists at Settings → Subscriptions (
SubscriptionsSettingsPage.tsx) and a real Subscribe CTA with a tier picker renders on public creator profiles;renewSubscription,enableConversationSubscription/disableConversationSubscriptionare all wired to real buttons. New surfaces:SubscriptionOffersPage.tsx, the public/offer/[token]route (OfferRedemptionPage.tsx),useApplicableDiscount.ts+DiscountedPrice.tsx. - Source:
apps/backend/graphql/resolvers/subscription-offer.resolver.js,apps/backend/graphql/types/subscription-offer.type.js,apps/backend/managers/payment-managers/subscription-offer.manager.js,apps/backend/managers/payment-managers/user-subscription.manager.js,apps/frontend-nextjs/src/page-components/settings/SubscriptionsSettingsPage.tsx. - Path fixes:
subscription-tier.access-service.js/user-subscription.access-service.jsmoved underdata-access-services/payment/.
Exclusive (paid) posts
- Per-photo free previews:
PostMedia.isPreview/PostMediaInput.isPreviewlet a creator mark specific media on a paid post as free (migration20260721180000-add-is-preview-to-post-media).Post.medianow returns those preview items to a locked viewer instead of resolving tonull; the toggle UI lives inCreatePostModal.tsx. - Discount bookkeeping: new
Post.originalCoinPrice(struck-through prior price when a creator lowerscoinPriceon an already-paid post; migration20260721200000-add-original-coin-price-to-post) andPost.hiddenMediaCount(count of still-locked media for a viewer without access). - UI correction: there is no dedicated "Paid content" profile tab — exclusive posts render inline in the normal feed/profile grid with a coin-price badge; the lock overlay + "Unlock for N coins" flow lives in
PostCard.tsx/PostModal.tsx. - Source:
apps/backend/graphql/types/post.type.js,apps/backend/managers/post-managers/post.manager.js(updatePostdiscount bookkeeping),apps/backend/database/models/PostMedia.js.
Creator analytics & payouts
- GraphQL: newly documented
subscriberRetention,myEarningsStatement(year)(EarningsStatement), andmyEarningsTimeSeries(days)(EarningsTimePoint, daily zero-filled trend). - Identity gate: both
requestCashout(Stripe) andrequestManualCashoutnow require an admin-approved identity verification before any coins move — a hard server-side gate (coin-cashout.manager.js→identityVerificationManager.requireApprovedForCashout), surfaced by a gate banner on the payouts screen. - UI routing map corrected: payout UI now lives at
/settings/payouts(PayoutsPage.tsx), earnings/statements at/settings/insights-tools(InsightsAndToolsPage.tsx), and per-buyer sales at/settings/sales(CreatorSalesPage.tsx). New client-side CSV export (handleExportCsv) andEarningsTrendChart.tsx/EarningsStatementCard.tsxcomponents. CoinCashout.statuslifecycle is provider-specific: Stripepending→paid/failed(+ admin-cancel→canceled); Manualpending→processing→completed/rejected.- Source:
apps/backend/managers/coin-managers/coin-cashout.manager.js,apps/backend/graphql/types/coin-transaction.type.js,apps/frontend-nextjs/src/page-components/settings/PayoutsPage.tsx.
Gifts
- GraphQL: new
Post.giftStats: GiftTargetStats!field bundles gift totals onto feedPostobjects (resolved per-post viagift.manager#getStatsForTarget), replacing a per-cardgiftStatsForTargetfetch. sendGiftnow also creates agift-type notification for the recipient and computes the sender'snewBalance(both best-effort, non-blocking).- Correction:
myGiftablePostsis not part of the gift system — it belongs to the top-fans reward flow (top-fans.*). - Source:
apps/backend/graphql/types/post.type.js,apps/backend/managers/coin-managers/gift.manager.js,apps/backend/data-access-services/gift/gift.access-service.js.
Post promotions
- GraphQL:
trackPromotionImpression/trackPromotionClickare now live and wired —PostCard.tsxfires them for active sponsored posts, so impressions/clicks increment for real.estimatePromotionReachis a real estimate derived from follower count + recent engagement + budget/duration (still ignores targeting). - Corrected input/return shapes:
createPostPromotiontakesCreatePostPromotionInput;updatePostPromotiontakesUpdatePostPromotionInput; pause/resume/cancel and admin approve/reject all return the updatedPostPromotionobject, not a{ success }wrapper. - UI: the full creator-facing promotions UI (
MyPromotionsPage.tsx+CreatePromotionModal.tsxat/settings/promotions) was already wired — the prior "no frontend UI at all" claim was wrong. - Source:
apps/backend/graphql/resolvers/post-promotion.resolver.js,apps/backend/graphql/types/post-promotion.type.js,apps/backend/managers/post-managers/post-promotion.manager.js.
Feed, content & media
Home feed — inline viewer state
- GraphQL: new
Postfield resolversviewerHasLiked/viewerHasSaved/viewerHasReposted/interactionCount/giftStatsbundle per-viewer state and counts directly into thefeed/userPosts/postqueries, replacing per-card round trips (PostCard.tsxfalls back to the old separate queries only when inline state is absent). - Feed personalization: a post the viewer just published (last 5 minutes) is pinned to the top of their own feed (
post.access-service.js#getFeed,FRESH_OWN_MS). PostMediagainedfilterCss,objectFit, andisPreview;PostTagInput.mediaIndex/x/yare now optional (positionless people-tags).- Corrected
reactToCommentexample: the argument isreaction(notreactionType) and it returns a plainBoolean; read counts viapostComment(id).reactionCounts/.viewerReaction. - Source:
apps/backend/graphql/types/post.type.js,apps/backend/graphql/resolvers/post.resolver.js,apps/frontend-nextjs/src/components/PostCard.tsx,apps/frontend-nextjs/src/page-components/HomePage.tsx.
Text-post backgrounds (new post.metadata)
- Model: a freeform
metadataJSONB column onpost(migration20260722000000-add-metadata-to-post) stores the Facebook-style background for text-only posts, e.g.{ background: 'linear-gradient(...)', textColor: '#fff' }. - Source:
apps/backend/database/migrations/20260722000000-add-metadata-to-post.js,apps/backend/database/models/Post.js.
Clips
- UI: logged-out browsing — a public Clips grid (
PublicClipsPage.tsx) is served viaBrowseGate.tsxat/clipsfor unauthenticated visitors; the Clips link now sits immediately after Explore in the desktop sidebar and is the center item of the mobile bottom nav. - GraphQL: the clips resolver passes
publicOnly: truefor anonymous callers; underpublicOnly,postAccessService.getByType('clip', …)filtersvisibility='public',isPublished=true, and excludes private-account creators. - Correction: the dedicated clip-creation flow is not wired —
CreateClipModal.tsxexists but isn't mounted by any route (the upload FAB was removed fromClipsPage.tsx). - Source:
apps/frontend-nextjs/src/page-components/PublicClipsPage.tsx,apps/frontend-nextjs/src/components/public/BrowseGate.tsx,apps/backend/data-access-services/post/post.access-service.js.
Stories & live
- Live coin gifts (
sendLiveGift/liveGiftCatalog/liveStreamGifts) and live-stream speaker requests (requestLiveSpeaker/liveStreamSpeakerRequests/myLiveSpeakerStatus/respondToLiveSpeakerRequest) are fully built and wired — flipped from "not built" to done. Live speaker requests are a separate implementation inlive-stream.manager.js, independent of the Call-only speaker-request manager. - New
promoteLiveStreammutation: the owner creates/reuses a companion announcement post and promotes it through the normal post-promotion flow ("Promote" button in the live room). - Corrections: story audience (
close_friends) is stored but not enforced on read;storyViewers/storyViewerCountare declared and called by the UI but throw at runtime (no manager methods back them);deleteLiveCommentis gated to the comment author or stream host (no admin bypass). - Source:
apps/backend/graphql/types/live-stream-gift.type.js,apps/backend/managers/live-stream-managers/live-stream.manager.js,apps/frontend-nextjs/src/page-components/LiveRoomPage.tsx.
Post scheduling & collaborators
- Scheduling: new
updateScheduledPostmutation and story scheduling (StoryCreateInput.scheduledAt); the owner now sees their own scheduled posts on their own profile grid (badged with the release time), while other viewers never do. A shared calendar-styleDateTimePicker(inpackages/ui) replaces the old datetime-local picker. - Collaborators: the
PostCollaboratorGraphQL surface (inviteCollaborator,acceptCollaboration,postCollaborators,collaborationStats, …) is now fully wired and documented;CollaboratorsModal.tsxnow callsinviteCollaborator/removeCollaborator/bulkInviteCollaboratorsfor real (fixed in a later cycle — see Post collaborators, technical reference), and accept/reject inCollaborationInvitesPage.tsxstill work. Accepted-collaborator posts do propagate to profiles/followers' feeds. - Source:
apps/backend/graphql/types/post.type.js,apps/backend/graphql/resolvers/post-collaborator.resolver.js,apps/backend/managers/post-managers/post.manager.js.
Content protection (watermarking)
- Correction:
DynamicWatermarkis now wired on only the live-stream surface. It was removed from exclusive-post views (PublicProfilePage.tsx/PostModal.tsx) and from paid chat message bubbles (MessageBubble.tsx) — the two "exclusive-post media" and "paid message image" checklist items were unchecked. The component itself is unchanged; two of its three call sites were deleted. - Source:
apps/frontend-nextjs/src/page-components/LiveRoomPage.tsx,apps/frontend-nextjs/src/components/common/DynamicWatermark.tsx.
Hashtags & mentions
trendingHashtags,hashtagByName,hashtagStatsare now correctly documented as consumed byHashtagPage.tsx.PostCard.tsxswitched tolinkifyText(), which linkifies both#hashtagsand@mentionsin captions.- Bug documented:
removeTag's SDL declaresuserId/contentType/contentIdbut the resolver destructures a never-providedtagId, so every call fails; thementions-tags.manager.jsmethods are still mock stubs. The real mention-removal path isremoveMention(post-mention.manager.js#deleteMention). - Path fixes:
hashtag.access-service.js→data-access-services/post/; mentions/tags resolvers →user-mentions-tags.resolver.js.
Messaging
In-chat translation (new)
- GraphQL: new
messageTranslations,myTranslationSettingqueries andsetMessageTranslationmutation, plusConversationParticipant.translationEnabled/preferredLanguagestorage (migration20260721160000-add-chat-translation). - Backend is provider-agnostic —
message-translation.service.jspicks the first configured provider (LibreTranslate → DeepL → Google → OpenAI) and no-ops if none is set. See the new env vars in Deployment. - Source:
apps/backend/graphql/types/message-translation.type.js,apps/backend/graphql/resolvers/message-translation.resolver.js,apps/backend/managers/message-managers/message-translation.manager.js,apps/backend/database/models/ConversationParticipant.js.
Chat features now wired
- Read receipts (
messageReadsubscription) are fully wired —useChatMessages.tsflips message status toreadandMessageBubble.tsxrenders the blue double-check. - Disappearing / expiring messages (
setMessageExpiration) wired viauseChatActions.tsand the message context menu;updateConversationSettingsmaps disappearing-messages toggles (default 86400s). - Static location share (
shareLocation) is wired from the composer and rendered with a Maps link; live location (updateLiveLocation/stopLiveLocation) remains unwired. - New:
messageReactionAddedsubscription (live-refreshes reactions);@mentionsparse intoMessageMentionrows oncreateMessage(no frontend consumer for the mention queries yet); invite-link flow backed byconversation-invite.manager.js. - The
MessageTypeenum code sample was fixed to includelinkandpost. - Path fix:
message.access-service.jslives underdata-access-services/message/.
Social, discovery & profile
Discover people & recommendations
- UI: a dedicated Discover People page at
/discover-people(DiscoverPeoplePage.tsx) with recommended / similar / people-you-may-know / trending / nearby sections, reached fromSuggestedUsers.tsx's "See all". Suggested-user cards now show a "Followed by [name] + N others" label. - GraphQL: new
User.followContext(MutualFollowContext) andUser.isFollowingfields; newdismissSuggestion(userId)mutation (persisted in thedismissed_suggestiontable) used byRecommendationsSection.tsx's per-card X button;usersYouMayKnow/similarUsersare wired. RecommendationsSection.tsx's category tab bar (may-know/similar/trending/recommended/nearby) is real and clickable (onClick={() => setActiveTab(tab.id)}), so every tab includingnearbyUsersis reachable through the UI (fixed in a later cycle — this doc previously said the tabs were hidden andactiveTabwas hardcoded);hideFromSuggestionsis called from both this component andAccountPrivacyPage.tsx.- Logged-out
/searchnow branches: authenticated →ExplorePage(hideRecommendations), logged-out →PublicSearchPage.tsx(searchUsers+searchHashtags). - Source:
apps/frontend-nextjs/src/page-components/DiscoverPeoplePage.tsx,apps/backend/graphql/resolvers/user-fields.resolver.js,apps/backend/graphql/types/user-recommendations.type.js.
Contacts & validation
- The old
user.resolver.jsis gone — contacts resolvers split intouser-contact-import.resolver.js,user-recommendations.resolver.js,user-mentions-tags.resolver.js,user-validation.resolver.js. - GraphQL corrections: real-time validation queries are
validateUsername/validateEmail(notisUsernameAvailable/isEmailAvailable);importContactstakesinput: ContactImportInput!;syncContactstakes no args; the real invite mutation isinviteContacts(emails). Contact suggestions (contactSuggestions) return the typedContactSuggestionsResponse(fixed in a later cycle — see Contacts, technical reference). - Bugs fixed in a later cycle (see Contacts, technical reference):
@-mention autocomplete (mentionSuggestions) now runs a real search and returns the schema-shaped[MentionSuggestion!]!;validateFieldRealtimenow forwards the right args to the manager. - Validation rule change: minimum registration age is now 18 (was 16/13) and
dateOfBirthis required at registration, since the platform sells paid intimate content (user.validator.js,validateDateOfBirth). - UI now exists:
RecommendationsSection.tsx,PostModal.tsx(mention suggestions),Login.tsx(validation).
Profile
- Social links are now a live, fully-wired GraphQL feature (
mySocialLinks/userSocialLinks/socialLinkStats;addSocialLink/updateSocialLink/deleteSocialLink/reorderSocialLinks/trackSocialLinkClick) surfaced at Settings → Social Links. user.resolver.js/user.access-service.jsno longer exist — the resolver was split into ~15 per-concern files auto-merged bygraphql/resolvers.js; the access service moved todata-access-services/user/.- Large GraphQL corrections:
Interestsis now a simpleUser.interests: [String!]updated viaupdateInterests; tag-permissions query istagPermissions(rawJSON!input); trending/online renamed togetTrendingUsers/getOnlineUsers;followUseralways follows immediately (private-account entry point issendFollowRequest); restriction-appeals signatures corrected;deleteAccountImmediately/bulkMuteUsersdocumented.
Favorites, saved posts, discovery odds & ends
- Favorites can now be added from a profile's "⋯ More options" menu (not just removed from Settings). A 50-favorite cap is enforced server-side. Corrections:
getFavoritesStatsonly computestotalFavorites(mutual/recent/mostFavorited are hardcoded 0/null); the manager is real Sequelize persistence (not "mock scaffolding"); data-access service moved todata-access-services/user/. - Saved posts now organize by collection —
mySavedPostsis gone, replaced bycollectionPosts(collectionId, …). Every user gets an auto-created, non-deletable default "Saved" collection (SavedCollection.isDefault, migration20260715090000from a prior cycle);deleteCollectionrejects the default. New UI:SaveToCollectionModal,SavedPage.tsx(/settings/saved),DiscoverCollectionsPage.tsx(/settings/discover-collections). - Location:
nearbyUsersis now surfaced on the/discover-people"Cerca de ti" section; the in-chat static location share is wired (see Messaging). - Referrals: the referrer now gets a
referral_bonusnotification; the referral page has native/WhatsApp/Facebook/X share buttons.
Accounts, auth & security
Authentication & 2FA
- SMS/email-method self-enrollment 2FA mutations (
enableTwoFactor,verifyTwoFactorCode,generateBackupCodes,verifyBackupCode) were removed from the schema — only authenticator-app 2FA can be self-enrolled now. The live setup mutations aresetupTwoFactor/verifyTwoFactorSetup/disableTwoFactor/regenerateBackupCodes. - New documentation for
verifyLoginTwoFactor(twoFactorToken, code)— the mutation that completes a login once 2FA is required (accepts a TOTP or a backup code). Login returnsrequiresTwoFactor/twoFactorToken/twoFactorMethod. regenerateBackupCodes,myPasskeys,deletePasskeyare now wired in Settings → Security.TwoFactorSetupResponsegainedqrCodeSvg(CSP-safe inline SVG) andotpauthUrl.- Apple Sign-In doc pointer redirected from the deleted
APPLE_SIGNIN_IMPLEMENTATION.mdto theAPPLE_*vars in environment-setup. - Source:
apps/backend/graphql/types/two-factor-auth.type.js,apps/backend/graphql/resolvers/user-authentication.resolver.js,apps/frontend-nextjs/src/components/Login.tsx,apps/frontend-nextjs/src/page-components/settings/SecuritySettingsPage.tsx.
Security, sessions & account management
- Security UI now exists in
frontend-nextjs:SecuritySettingsPage.tsx(/settings/security),SecurityAlertsPage.tsx,SessionsSettingsPage.tsx,VerificationRequestPage.tsx. The olduser.resolver.jswas split intouser-two-factor-auth/user-security-alerts/user-sessions/user-authenticationresolvers. - Session bugs documented:
terminateAllSessionsis an alias ofrevokeAllOtherSessions(does not sign out the calling device);loginHistorythrows (shape mismatch);sessionsTerminatedalways resolvesnull(manager returnsrevoked_count);getCurrentSessionreturns hardcoded mock data. - Account management:
getActivityHeatmapis now visualized as a GitHub-style contribution heatmap on Settings → Activity insights.deleteAccountis a grace-period finalizer (rejects unless alreadypending_deletionand past the scheduled date);deleteAccountImmediatelyis the real bypass.setAllNotificationsno longer exists. The memorialized-accounts stack (user + admin GraphQL) is now fully implemented. The email service is a multi-provider facade (services/email/,EMAIL_PROVIDER). - Notifications: new
notificationAdded(userId)subscription (frontend subscribes instead of polling); theNotificationTypeenum grew from 8 to 34 values; push is now provider-agnostic (FCM viaservices/push-notification/), with VoIP-for-calls still on real APNs (voip-push.service.js). Data-access service moved todata-access-services/user/. - Data export is fully implemented now: a
data_exporttable (migration20260719080000, prior cycle),myDataExports/downloadDataExport/processDataExport, adataExportStatusChangedsubscription, andDownloadDataPage.tsx(/settings/download-data). Several fields are documented as partial/broken (getDataPortabilityInfo,downloadToken,estimatedCompletion).
Settings, profile-viewers, age gate
- Restrict (
restrictAccount/unrestrictAccount) and Limit interactions (setInteractionLimit) are fully wired end-to-end (migration20260719030000, prior cycle) with real settings pages.InsightsAndToolsPagenow shows real creator monetization analytics. Family Center — built in a later cycle, no longer a stub — is documented separately at Family Center. hide_profile_visitsis now fully wired (privacy toggle →recordVisitgate), not dead code.- Age/identity: the identity-verification gate banner now also appears on a dedicated
/settings/payoutspage.
Moderation & admin
Account moderators (expanded)
- Permissions grew from 5 to 9 (added Post stories, Comment, React, Manage comments), with a step-up re-auth mechanism (
assertRecentAuth/verifyPasswordStepUpbacked by a Redis 15-minute window instep-up-auth.service.js) and a newverifyPasswordForModerators(password)mutation. - New audit trail:
AccountModeratorActionmodel +account-moderator-action.access-service.js+moderatorActivityquery, andMessage.viaModeratorattribution (migration20260721150000-expand-account-moderator). - Correction:
canStoryis defined but not enforced anywhere —createStoryis a separate mutation that never acceptsactAsUserId. The "Comentar como" comment-as-owner flow lives inPostModal.tsx+post-comment.resolver.js. - Source:
apps/backend/graphql/types/account-moderator.type.js,apps/backend/managers/user-managers/account-moderator.manager.js,apps/backend/services/step-up-auth.service.js,apps/backend/database/models/AccountModeratorAction.js.
Admin passkeys & password reset
- Admin passkeys: WebAuthn credentials scoped to
admin_user(newadmin_passkeytable, migration20260721140000-create-admin-passkey). - Password reset:
adminRequestPasswordReset(unauthenticated, always reports success to avoid email enumeration) andadminResetPassword(consumes a one-time token) wired to new/forgot-passwordand/reset-passwordadmin pages. - Source:
apps/backend/graphql/resolvers/admin/admin-user.resolver.js,apps/backend/managers/admin-managers/admin-user.manager.js,apps/frontend-admin/src/app/forgot-password/page.tsx.
Content moderation
- Full admin moderation dashboard now exists in
frontend-admin:/moderation(report queue),/moderation/flagged(flagged-content queue + actions),/moderation/rules(auto-moderation rule CRUD) — the prior "no admin dashboard" claims were corrected. - Auto-moderation rules engine is real and enforced:
adminGetAutoModerationRules/Create/Update/Delete against anauto_moderation_ruletable, enforced server-side by a newservices/auto-moderation.service.jscalled frompost.manager.json post creation. - Corrections:
adminEscalateReport/adminDismissReportwere removed (useadminReviewReportwithESCALATE/REJECT);content-moderation.resolver.js/.type.jsmoved underadmin/;ContentModerationDetails.nsfwScorenow has a real resolver (posts only);toxicityScoreis always null;adminGetContentModerationHistoryreturns[](missingfindByContent). - New backend files:
auto-moderation.service.js,content-preview.service.js,content-moderation-log.access-service.js.
Dashboard analytics, identity, payments, disputes, user-moderation
- Dashboard:
UserGrowthData.retentionRateprefers real signup-cohort data (user_cohort_snapshots, added in a later cycle — see Dashboard analytics), falling back to the original returning-user proxy (null only when no prior base) for buckets not yet covered by a snapshot;adminGetActivityLognow mergesuser_moderation_logs+content_moderation_logs. - Identity verification (admin):
adminPendingIdentityVerificationsreturns a flat[IdentityVerificationRequest!]!list (paged by limit/offset), andadminApproveIdentityVerification/adminRejectIdentityVerificationreturnIdentityVerificationStatusResult!(rejectreasonis required). - Payments/payouts (admin): new
adminGetCashout,adminGetPayoutProfile,adminSetPayoutAccountDisabled;adminGetCashoutscorrected to takefilters/limit/offsetand return[CoinCashout!]!; cancel/complete/reject mutations takeidand returnCoinCashout!. - Shop disputes:
adminOpenProductDisputes/adminResolveProductDisputelive on the admin schema (/admin/graphql), gated bycontext.admin+MODERATE_CONTENT;ProductDisputeResolutionvalues areresolved_refund(reverses coins, doesn't restore stock) /resolved_denied. - User moderation:
adminGetSuspendedUsers/adminGetBannedUsers/adminGetUserSuspensions/adminGetUserWarningswere removed;adminBulkBanUsers(super-admin only) exists but has no admin UI yet. - Appeals: denial notes are optional (no presence check in resolver or UI).
- User feedback: the GraphQL surface (
myFeedback,publicFeedback,submitFeedback,upvoteFeedback/downvoteFeedback) is live with a realFeedbackPage.tsx; admin review tooling (adminGetFeedback/adminGetFeedbackStats/adminUpdateFeedbackStatus/adminSetFeedbackPriority/adminRespondToFeedback, gated byMANAGE_FEEDBACK, with a real/feedbackadmin UI) was fixed in a later cycle — see User Feedback (admin). Vote dedup viaUserFeedbackVote(migration20260718140000, prior cycle).
Deployment & configuration
Run the new database migrations
8 new migrations landed this cycle. From apps/backend:
npx sequelize-cli db:migrate
# then restart the backend
| Migration | Adds | Track |
|---|---|---|
20260721140000-create-admin-passkey | admin_passkey table (WebAuthn credentials for admin login) | Admin passkeys |
20260721150000-expand-account-moderator | can_comment / can_manage_comments / can_story / can_react on account_moderator; new account_moderator_action audit table | Account moderators |
20260721160000-add-chat-translation | translation_enabled / preferred_language on conversation_participant; new message_translation cache table | Chat translation |
20260721170000-create-subscription-offer | subscription_offer + subscription_offer_redemption tables (trials / discounts) | Subscription offers |
20260721180000-add-is-preview-to-post-media | post_media.is_preview | Per-photo free previews |
20260721190000-create-platform-wallet | platform_wallet table (seeded with one platform row) | Platform coin wallet |
20260721200000-add-original-coin-price-to-post | post.original_coin_price | Paid-post discount pricing |
20260722000000-add-metadata-to-post | post.metadata (JSONB — text-post backgrounds) | Post metadata |
All migrations are idempotent (guarded by describeTable/showAllTables), so re-running is safe.
The many doc-reconciliation changes above (mutations added, removed, corrected, or file-path fixes) reuse existing schema and require no migration of their own — just a backend restart to pick up resolver changes. Tables referenced above but not in this table (e.g. content_moderation_logs via 20260717060000, data_export via 20260719080000, saved_collection.is_default via 20260715090000, interaction limits via 20260719030000) were created in prior cycles.
New environment variables — chat translation (all optional)
Chat translation is provider-agnostic and disabled by default: message-translation.service.js picks the first configured provider, and if none is set it no-ops (logs "chat translation disabled"). To enable it, set the credentials for exactly one provider.
Provider selection order (first configured wins): LibreTranslate → DeepL → Google → OpenAI.
| Variable | Purpose | Default |
|---|---|---|
LIBRETRANSLATE_URL | LibreTranslate base URL (its presence selects this provider) | — |
LIBRETRANSLATE_API_KEY | Optional LibreTranslate API key | — |
DEEPL_API_KEY | DeepL auth key (selects DeepL) | — |
DEEPL_API_URL | DeepL API base URL | https://api-free.deepl.com |
GOOGLE_TRANSLATE_API_KEY | Google Translate v2 API key (selects Google) | — |
OPENAI_API_KEY | OpenAI key (selects OpenAI) | — |
OPENAI_TRANSLATE_MODEL | OpenAI model for translation | gpt-4o-mini |
TRANSLATION_MAX_CHARS | Max characters per message translated | 5000 |
TRANSLATION_MONTHLY_CHAR_LIMIT | Monthly character cap (0 = no limit) | 0 |
TRANSLATION_HTTP_TIMEOUT_MS | Per-request timeout to the provider | 8000 |
None of these are required to boot the backend — leaving them unset simply keeps chat translation off.
The multi-provider email (
EMAIL_PROVIDER) and push-notification (PUSH_NOTIFICATION_PROVIDER,DISABLE_PUSH_NOTIFICATIONS,FIREBASE_*,APNS_*) facades were documented this cycle as corrections — the code and those variables already existed before this cycle, so they are not new configuration requirements. See Environment Setup for the full reference.