Skip to main content

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/createCoinPurchase mutations are gone. The new flow is createCoinPurchaseIntent/confirmCoinPurchaseIntent (Stripe PaymentIntent, backs Apple Pay / Google Pay) and createPaypalCoinOrder/capturePaypalCoinOrder (PayPal), returning the new StripeCoinIntent / PaypalCoinOrder types.
  • UI: a new CoinExpressCheckout.tsx (Stripe Express Checkout Element + PayPal Buttons) renders above StripeCheckout in the coins payment modal. Coin-package selection moved to /settings/get-coins (GetCoinsPage.tsx); /coins now redirects there.
  • Model: a CoinPurchase links to a PaymentTransaction whose provider is a free-form string ('stripe' or 'paypal'), not Stripe-only.
  • The CoinTransactionType enum grew from 6 to 16 values (adds tip_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 (PlatformWallet model + migration 20260721190000-create-platform-wallet, seeded with one platform row) and its manager.
  • GraphQL (admin): adminPlatformWallet, adminGetUserCoinBalance queries; adminTopUpWallet, adminTransferCoinsToUser, adminTakeCoinsFromUser mutations.
  • UI: UserCoinsCard.tsx on 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.js and replaced by admin-namespaced equivalents (adminCreateCoinPackage/adminUpdateCoinPackage/… plus adminGetCoinPackages).

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 (migration 20260721170000).
  • GraphQL: new subscriberRetention(months) query (SubscriberRetentionPoint) — month-bucketed churn/retention analytics, computed in application code from the creator's full UserSubscription history.
  • 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/disableConversationSubscription are 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.js moved under data-access-services/payment/.

Exclusive (paid) posts

  • Per-photo free previews: PostMedia.isPreview / PostMediaInput.isPreview let a creator mark specific media on a paid post as free (migration 20260721180000-add-is-preview-to-post-media). Post.media now returns those preview items to a locked viewer instead of resolving to null; the toggle UI lives in CreatePostModal.tsx.
  • Discount bookkeeping: new Post.originalCoinPrice (struck-through prior price when a creator lowers coinPrice on an already-paid post; migration 20260721200000-add-original-coin-price-to-post) and Post.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 (updatePost discount bookkeeping), apps/backend/database/models/PostMedia.js.

Creator analytics & payouts

  • GraphQL: newly documented subscriberRetention, myEarningsStatement(year) (EarningsStatement), and myEarningsTimeSeries(days) (EarningsTimePoint, daily zero-filled trend).
  • Identity gate: both requestCashout (Stripe) and requestManualCashout now require an admin-approved identity verification before any coins move — a hard server-side gate (coin-cashout.manager.jsidentityVerificationManager.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) and EarningsTrendChart.tsx/EarningsStatementCard.tsx components.
  • CoinCashout.status lifecycle is provider-specific: Stripe pending→paid/failed (+ admin-cancel→canceled); Manual pending→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 feed Post objects (resolved per-post via gift.manager#getStatsForTarget), replacing a per-card giftStatsForTarget fetch.
  • sendGift now also creates a gift-type notification for the recipient and computes the sender's newBalance (both best-effort, non-blocking).
  • Correction: myGiftablePosts is 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/trackPromotionClick are now live and wired — PostCard.tsx fires them for active sponsored posts, so impressions/clicks increment for real. estimatePromotionReach is a real estimate derived from follower count + recent engagement + budget/duration (still ignores targeting).
  • Corrected input/return shapes: createPostPromotion takes CreatePostPromotionInput; updatePostPromotion takes UpdatePostPromotionInput; pause/resume/cancel and admin approve/reject all return the updated PostPromotion object, not a { success } wrapper.
  • UI: the full creator-facing promotions UI (MyPromotionsPage.tsx + CreatePromotionModal.tsx at /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 Post field resolvers viewerHasLiked/viewerHasSaved/viewerHasReposted/interactionCount/giftStats bundle per-viewer state and counts directly into the feed/userPosts/post queries, replacing per-card round trips (PostCard.tsx falls 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).
  • PostMedia gained filterCss, objectFit, and isPreview; PostTagInput.mediaIndex/x/y are now optional (positionless people-tags).
  • Corrected reactToComment example: the argument is reaction (not reactionType) and it returns a plain Boolean; read counts via postComment(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 metadata JSONB column on post (migration 20260722000000-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 via BrowseGate.tsx at /clips for 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: true for anonymous callers; under publicOnly, postAccessService.getByType('clip', …) filters visibility='public', isPublished=true, and excludes private-account creators.
  • Correction: the dedicated clip-creation flow is not wired — CreateClipModal.tsx exists but isn't mounted by any route (the upload FAB was removed from ClipsPage.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 in live-stream.manager.js, independent of the Call-only speaker-request manager.
  • New promoteLiveStream mutation: 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/storyViewerCount are declared and called by the UI but throw at runtime (no manager methods back them); deleteLiveComment is 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 updateScheduledPost mutation 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-style DateTimePicker (in packages/ui) replaces the old datetime-local picker.
  • Collaborators: the PostCollaborator GraphQL surface (inviteCollaborator, acceptCollaboration, postCollaborators, collaborationStats, …) is now fully wired and documented; CollaboratorsModal.tsx now calls inviteCollaborator/removeCollaborator/bulkInviteCollaborators for real (fixed in a later cycle — see Post collaborators, technical reference), and accept/reject in CollaborationInvitesPage.tsx still 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: DynamicWatermark is 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, hashtagStats are now correctly documented as consumed by HashtagPage.tsx. PostCard.tsx switched to linkifyText(), which linkifies both #hashtags and @mentions in captions.
  • Bug documented: removeTag's SDL declares userId/contentType/contentId but the resolver destructures a never-provided tagId, so every call fails; the mentions-tags.manager.js methods are still mock stubs. The real mention-removal path is removeMention (post-mention.manager.js#deleteMention).
  • Path fixes: hashtag.access-service.jsdata-access-services/post/; mentions/tags resolvers → user-mentions-tags.resolver.js.

Messaging

In-chat translation (new)

  • GraphQL: new messageTranslations, myTranslationSetting queries and setMessageTranslation mutation, plus ConversationParticipant.translationEnabled/preferredLanguage storage (migration 20260721160000-add-chat-translation).
  • Backend is provider-agnosticmessage-translation.service.js picks 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 (messageRead subscription) are fully wired — useChatMessages.ts flips message status to read and MessageBubble.tsx renders the blue double-check.
  • Disappearing / expiring messages (setMessageExpiration) wired via useChatActions.ts and the message context menu; updateConversationSettings maps 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: messageReactionAdded subscription (live-refreshes reactions); @mentions parse into MessageMention rows on createMessage (no frontend consumer for the mention queries yet); invite-link flow backed by conversation-invite.manager.js.
  • The MessageType enum code sample was fixed to include link and post.
  • Path fix: message.access-service.js lives under data-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 from SuggestedUsers.tsx's "See all". Suggested-user cards now show a "Followed by [name] + N others" label.
  • GraphQL: new User.followContext (MutualFollowContext) and User.isFollowing fields; new dismissSuggestion(userId) mutation (persisted in the dismissed_suggestion table) used by RecommendationsSection.tsx's per-card X button; usersYouMayKnow/similarUsers are wired.
  • RecommendationsSection.tsx's category tab bar (may-know/similar/trending/recommended/nearby) is real and clickable (onClick={() => setActiveTab(tab.id)}), so every tab including nearbyUsers is reachable through the UI (fixed in a later cycle — this doc previously said the tabs were hidden and activeTab was hardcoded); hideFromSuggestions is called from both this component and AccountPrivacyPage.tsx.
  • Logged-out /search now 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.js is gone — contacts resolvers split into user-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 (not isUsernameAvailable/isEmailAvailable); importContacts takes input: ContactImportInput!; syncContacts takes no args; the real invite mutation is inviteContacts(emails). Contact suggestions (contactSuggestions) return the typed ContactSuggestionsResponse (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!]!; validateFieldRealtime now forwards the right args to the manager.
  • Validation rule change: minimum registration age is now 18 (was 16/13) and dateOfBirth is 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.js no longer exist — the resolver was split into ~15 per-concern files auto-merged by graphql/resolvers.js; the access service moved to data-access-services/user/.
  • Large GraphQL corrections: Interests is now a simple User.interests: [String!] updated via updateInterests; tag-permissions query is tagPermissions (raw JSON! input); trending/online renamed to getTrendingUsers/getOnlineUsers; followUser always follows immediately (private-account entry point is sendFollowRequest); restriction-appeals signatures corrected; deleteAccountImmediately/bulkMuteUsers documented.

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: getFavoritesStats only computes totalFavorites (mutual/recent/mostFavorited are hardcoded 0/null); the manager is real Sequelize persistence (not "mock scaffolding"); data-access service moved to data-access-services/user/.
  • Saved posts now organize by collection — mySavedPosts is gone, replaced by collectionPosts(collectionId, …). Every user gets an auto-created, non-deletable default "Saved" collection (SavedCollection.isDefault, migration 20260715090000 from a prior cycle); deleteCollection rejects the default. New UI: SaveToCollectionModal, SavedPage.tsx (/settings/saved), DiscoverCollectionsPage.tsx (/settings/discover-collections).
  • Location: nearbyUsers is 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_bonus notification; 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 are setupTwoFactor/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 returns requiresTwoFactor/twoFactorToken/twoFactorMethod.
  • regenerateBackupCodes, myPasskeys, deletePasskey are now wired in Settings → Security. TwoFactorSetupResponse gained qrCodeSvg (CSP-safe inline SVG) and otpauthUrl.
  • Apple Sign-In doc pointer redirected from the deleted APPLE_SIGNIN_IMPLEMENTATION.md to the APPLE_* 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 old user.resolver.js was split into user-two-factor-auth/user-security-alerts/user-sessions/user-authentication resolvers.
  • Session bugs documented: terminateAllSessions is an alias of revokeAllOtherSessions (does not sign out the calling device); loginHistory throws (shape mismatch); sessionsTerminated always resolves null (manager returns revoked_count); getCurrentSession returns hardcoded mock data.
  • Account management: getActivityHeatmap is now visualized as a GitHub-style contribution heatmap on Settings → Activity insights. deleteAccount is a grace-period finalizer (rejects unless already pending_deletion and past the scheduled date); deleteAccountImmediately is the real bypass. setAllNotifications no 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); the NotificationType enum grew from 8 to 34 values; push is now provider-agnostic (FCM via services/push-notification/), with VoIP-for-calls still on real APNs (voip-push.service.js). Data-access service moved to data-access-services/user/.
  • Data export is fully implemented now: a data_export table (migration 20260719080000, prior cycle), myDataExports/downloadDataExport/processDataExport, a dataExportStatusChanged subscription, and DownloadDataPage.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 (migration 20260719030000, prior cycle) with real settings pages. InsightsAndToolsPage now shows real creator monetization analytics. Family Center — built in a later cycle, no longer a stub — is documented separately at Family Center.
  • hide_profile_visits is now fully wired (privacy toggle → recordVisit gate), not dead code.
  • Age/identity: the identity-verification gate banner now also appears on a dedicated /settings/payouts page.

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/verifyPasswordStepUp backed by a Redis 15-minute window in step-up-auth.service.js) and a new verifyPasswordForModerators(password) mutation.
  • New audit trail: AccountModeratorAction model + account-moderator-action.access-service.js + moderatorActivity query, and Message.viaModerator attribution (migration 20260721150000-expand-account-moderator).
  • Correction: canStory is defined but not enforced anywhere — createStory is a separate mutation that never accepts actAsUserId. The "Comentar como" comment-as-owner flow lives in PostModal.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 (new admin_passkey table, migration 20260721140000-create-admin-passkey).
  • Password reset: adminRequestPasswordReset (unauthenticated, always reports success to avoid email enumeration) and adminResetPassword (consumes a one-time token) wired to new /forgot-password and /reset-password admin 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 an auto_moderation_rule table, enforced server-side by a new services/auto-moderation.service.js called from post.manager.js on post creation.
  • Corrections: adminEscalateReport/adminDismissReport were removed (use adminReviewReport with ESCALATE/REJECT); content-moderation.resolver.js/.type.js moved under admin/; ContentModerationDetails.nsfwScore now has a real resolver (posts only); toxicityScore is always null; adminGetContentModerationHistory returns [] (missing findByContent).
  • 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.retentionRate prefers 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; adminGetActivityLog now merges user_moderation_logs + content_moderation_logs.
  • Identity verification (admin): adminPendingIdentityVerifications returns a flat [IdentityVerificationRequest!]! list (paged by limit/offset), and adminApproveIdentityVerification/adminRejectIdentityVerification return IdentityVerificationStatusResult! (reject reason is required).
  • Payments/payouts (admin): new adminGetCashout, adminGetPayoutProfile, adminSetPayoutAccountDisabled; adminGetCashouts corrected to take filters/limit/offset and return [CoinCashout!]!; cancel/complete/reject mutations take id and return CoinCashout!.
  • Shop disputes: adminOpenProductDisputes/adminResolveProductDispute live on the admin schema (/admin/graphql), gated by context.admin + MODERATE_CONTENT; ProductDisputeResolution values are resolved_refund (reverses coins, doesn't restore stock) / resolved_denied.
  • User moderation: adminGetSuspendedUsers/adminGetBannedUsers/adminGetUserSuspensions/adminGetUserWarnings were 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 real FeedbackPage.tsx; admin review tooling (adminGetFeedback/adminGetFeedbackStats/adminUpdateFeedbackStatus/adminSetFeedbackPriority/adminRespondToFeedback, gated by MANAGE_FEEDBACK, with a real /feedback admin UI) was fixed in a later cycle — see User Feedback (admin). Vote dedup via UserFeedbackVote (migration 20260718140000, 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
MigrationAddsTrack
20260721140000-create-admin-passkeyadmin_passkey table (WebAuthn credentials for admin login)Admin passkeys
20260721150000-expand-account-moderatorcan_comment / can_manage_comments / can_story / can_react on account_moderator; new account_moderator_action audit tableAccount moderators
20260721160000-add-chat-translationtranslation_enabled / preferred_language on conversation_participant; new message_translation cache tableChat translation
20260721170000-create-subscription-offersubscription_offer + subscription_offer_redemption tables (trials / discounts)Subscription offers
20260721180000-add-is-preview-to-post-mediapost_media.is_previewPer-photo free previews
20260721190000-create-platform-walletplatform_wallet table (seeded with one platform row)Platform coin wallet
20260721200000-add-original-coin-price-to-postpost.original_coin_pricePaid-post discount pricing
20260722000000-add-metadata-to-postpost.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.

VariablePurposeDefault
LIBRETRANSLATE_URLLibreTranslate base URL (its presence selects this provider)
LIBRETRANSLATE_API_KEYOptional LibreTranslate API key
DEEPL_API_KEYDeepL auth key (selects DeepL)
DEEPL_API_URLDeepL API base URLhttps://api-free.deepl.com
GOOGLE_TRANSLATE_API_KEYGoogle Translate v2 API key (selects Google)
OPENAI_API_KEYOpenAI key (selects OpenAI)
OPENAI_TRANSLATE_MODELOpenAI model for translationgpt-4o-mini
TRANSLATION_MAX_CHARSMax characters per message translated5000
TRANSLATION_MONTHLY_CHAR_LIMITMonthly character cap (0 = no limit)0
TRANSLATION_HTTP_TIMEOUT_MSPer-request timeout to the provider8000

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.