dashboard-auth-servicesThe customer and bank dashboards' auth services. Cognito authenticates a session and nothing else: users, memberships, roles, invites and activity are rows these services own. At the root: cfxStaff.ts, the CFX-staff email rule these services and both dashboards share; DashboardSsoSignInService, which tells the login page which tenant IdP an email belongs to; DashboardSsoUserAdminService, an operator's repair of a user's Cognito sign-in links after an SSO connection is deleted; and DashboardInviteMailer, the type of the invite-email sender the invite services take. ./clients/cognito holds CognitoAuthClient and CognitoAdminClient. A workspace is named by b2b.customer.name or b2b.bank.name, which CFX admins change.
./customer holds the customer dashboard's services over b2b tables: customer_dashboard_user, customer_dashboard_membership (one membership per workspace and user), customer_dashboard_user_activity, plus customer_dashboard_invite, customer_dashboard_workspace and the RBAC catalog. Each takes { db }; the invite services also take an inviteMailer. Customer-scoped methods name the caller as { dashboardUserId, customerId }: the user's external id (cdu_…; bank bdu_…) and the customer's id. A user's cognitoSub is their login, null until linked, and never their key. Nothing branches on which dashboard calls; the bank dashboard gets its own copies. See docs/proposals/SHARED_DASHBOARD_AUTH.md.
b2b.customer_dashboard_workspace or bank.bank_dashboard_workspace, one per customer or bank, reached through the tenant's workspace_id (DASHBOARD_WORKSPACE_TABLES.md). It holds the role set the dashboard assigns from, which *RoleSetAdminService.assign switches by locking and writing that row, and for a bank its dashboard domain, theme and enabled flag. Memberships, invites, SSO connections and activity name only their workspace_id, and a user's last-used workspace is last_workspace_id: a query that starts from a customer or bank reaches them through the tenant's workspace_id, and one that reports a tenant joins it back the same way. Each table's one unique index is on workspace_id (one membership per workspace and user, one live PENDING invite per workspace and address, one live SSO connection per workspace). Services still address a tenant by customer or bank id; workspaceId on their results is the workspace's external id (bws_… for a bank).BankDashboardMembershipService.searchOwn returns each membership's workspaceId, dashboardDomain, dashboardEnabled and theme, and can be limited to banks whose dashboard is enabled. That is what bank-dashboard picks its one active bank from: the host is no part of it.CustomerDashboardMembershipService and CustomerDashboardInviteService authorize inside the query, joining the caller's ACTIVE membership to their role's grants; a member is addressed by customer and user id.CustomerDashboardMembershipService.searchOwn (bank: BankDashboardMembershipService.searchOwn) is the dashboard's per-request principal: the caller's own live memberships, with each one's grants and their user row. A DISABLED membership is listed with no grants, as is an internal role held by anyone but confirmed CFX staff. It needs no permission.*MembershipService.update with status (ACTIVE or DISABLED), which needs team/role:w and a member within the caller's reach who isn't the caller. A DISABLED member keeps their role. The change is recorded as user.workspaceStatusChanged in the same transaction. The last ACTIVE holder of the top role cannot be disabled (LAST_OWNER).inv_…) that is written, then emailed, and accepted by a sign-in that proves its address. *InviteService.create and *InviteAdminService.create take a dashboardOrigin and send the email through the inviteMailer they were built with. Every caller builds DashboardInviteEmailSender from @cfxlabsinc/notification-services/dashboard-invite, which sends through SES from the address its caller passes (NOTIFICATION_FROM_EMAIL in the three dashboards, DASHBOARD_INVITE_FROM_EMAIL in temporal-worker) with a link to <dashboardOrigin>/signup?inviteId=<inv_…>&email=<address>. A send that fails withdraws the row, so none stays PENDING for an address nobody emailed. The member-facing *InviteService.create refuses an address that is already a member (WORKSPACE_INVITE_REJECTED). CustomerDashboardInviteAdminService.create is also onboarding's owner invite. inviteAcceptableSince and inviteExpiresAt are exported from ./customer and ./bank.CustomerDashboardMembershipAdminService and BankDashboardMembershipAdminService are for callers with no dashboard user: CFX operators re-roling, disabling, re-enabling and removing members, and a Cognito sign-in accepting its invites. An operator's update keeps the LAST_OWNER rule and records nothing in the workspace's feed: admin-dashboard keeps its own activity log. acceptPendingInvites takes the proven address's PENDING invites from the last 14 days (INVITE_ACCEPTANCE_DAYS), decides each membership by the pure rules in src/membershipSync.ts, and records each join as activity in the same transaction.CustomerDashboardUserActivityService records and reads the activity feed. CustomerDashboardUserService (bank: BankDashboardUserService) reads users by id, including those who have left, and is the only writer of their rows: createFromCognito for an invitee's first sign-in, linkCognito, update (name, profile picture and phone number), and markSeen.CustomerDashboardPermissionAdminService reads the whole permission catalog.CustomerNotificationService.getRecipients resolves an event's email recipients for email-notification-handler, reading the matched members and their overlays in one query. CustomerDashboardUserNotificationPreferencesService reads and updates a member's own email toggles, and its update never creates a membership. The pure wantsEmail in src/customer/notificationPreferences.ts applies them.These services read no PropelAuth identifier. A customer's workspace key (workspaceId on these services) is its workspace row's external_id: the value the customer already had, or cws_… for one created since. A user is named only by their own cdu_… / bdu_… id: the PropelAuth user ids that historical JSONB held (transaction creators, terms acceptances, team activity rows, notification userIds) were rewritten to those ids.
The PropelAuth columns are gone: 20261010040000_drop_legacy_auth_columns.sql dropped b2b.customer.propelauth_workspace_id and workspace_external_id with the trigger customer_workspace_external_id_sync, b2b.bank.propelauth_org_id, propelauth_id on both dashboard user tables and propelauth_invite_id on both invite tables (COGNITO_AUTH_MIGRATION.md § Phase 2). The other columns the workspace tables replaced (customer.dashboard_role_set_id, bank.dashboard_role_set_id, bank.dashboard_domain, bank.theme) are neither read nor written by these services: they are frozen at the values the workspace rows were filled from, and their drop is still deferred (DASHBOARD_WORKSPACE_TABLES.md § 3).