sdm-area-foundationrun.md00_intake/stub.mdThe SDM "Go live readiness" dashboard (apps/web/app/(app)/sdm/dashboard/page.tsx)
is a "use client" page reading entirely from apps/web/lib/mock/sdm.ts,
with a hard-coded sdmName: "Marcus Chen". Two things block any section from being real: (1) the page
is not scoped to the signed-in SDM, and (2) there is no notion of an SDM's area — an SDM is meant to
own a slice of supply (regions and/or platforms), but no assignment exists, so "my area" vs "all" has
nothing to resolve against. Every section needs the same area-scoped set of leads + experts to
aggregate over.
sdm-supply-taxonomy) a user with
role sdm owns — so the dashboard can scope to "my area". Index it for per-SDM queries./sdm (and /sdm/dashboard) to a server-fetched page scoped to the signed-in SDM, with a
"my area" vs "all" toggle resolving to a real query (area-scoped leads/experts vs the wider tenant
set) instead of the static mock.@sustentus/services/server that the demand, supply, risk, expert-strength and analytics sections
all read from (one area query → the SDM's in-scope leads + experts).sdm can be assigned an area (regions/platforms); the assignment is
queryable/indexed per tenant./sdm is server-rendered and scoped to the signed-in SDM; "my area" shows only in-scope
leads/experts, the toggle widens to the tenant set.lib/mock/sdm.touches: packages/services/src/db/models/user.ts (SDM area fields on the sdm discriminator, or a
small assignment model), packages/services/src/server (area read-service),
apps/web/app/(app)/sdm/{page.tsx,dashboard/page.tsx} (server-fetch + scope).01_define/output/spec.mdThe web SDM "Go live readiness" dashboard (apps/web/app/(app)/sdm/dashboard/page.tsx) is a
"use client" page that reads entirely from apps/web/lib/mock/sdm.ts, with a hard-coded
sdmName: "Marcus Chen". Two things block any section from becoming real: (1) the page is not scoped
to the signed-in SDM, and (2) there is no notion of an SDM's area — an SDM is meant to own a slice
of supply (regions and/or technology platforms), but no assignment exists, so the dashboard's "my area"
vs "all" framing has nothing to resolve against. Every section (demand, supply, risk, expert strength,
analytics) needs the same area-scoped set of leads + experts to aggregate over. This is sequence 2 of 8
in the sdm-dashboard-data epic — it depends on sdm-supply-taxonomy (sequence 1, which establishes
the platform→module key) and is the prerequisite for the other six sections. It advances Build the
Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow by proving the SDM
dashboard can resolve real, tenant- and area-scoped supply/demand data end to end.
Add an SDM area assignment to the sdm user discriminator. The SDMSchema discriminator is
currently empty (new Schema<ISDMUser>({})), so it is the natural, lean home for the area (mirroring
how csm-portfolio-foundation extended an existing model rather than adding a new one). Add an area
sub-document holding the two dimensions an SDM owns:
regions: string[] — region keys matching lead.location.region (e.g. UK, DACH, APAC,
Nordics).platforms: ObjectId[] — references to the technology-platform taxonomy from sdm-supply-taxonomy
(ref: "taxonomy"), e.g. SAP / Oracle / Salesforce.Index it for per-SDM, tenant-scoped lookup (the SDM is resolved by tenantId + the signed-in user, so
the index follows the established multi-tenant pattern). Assignment is seeded / back-office this
round — the dashboard reads the area, it does not manage it.
Define area membership as a clear, testable rule. A lead or expert is in the SDM's area when it
matches every dimension the area specifies: if area.regions is non-empty, its region must be in
that set; if area.platforms is non-empty, its resolved platform must be in that set. A dimension left
empty is unconstrained. (Platform resolution for leads and experts is the sdm-supply-taxonomy
feature's job; this feature consumes that key. If the platform dimension is unset, the region dimension
alone scopes the area.)
Convert /sdm and /sdm/dashboard to a server component that resolves the Clerk-authenticated SDM
→ their tenant (getTenant) → the SDM app user (resolveAppUserForTenant, role === "sdm"), reusing
the same helpers the CSM and customer dashboards already use. The page header renders the real
signed-in SDM identity (name + role line), not the hard-coded Marcus Chen.
Stand up the shared area read-service sdmAreaService in @sustentus/services/server (new
db/services/sdm-area/ following the established index.ts class + instance.ts singleton pattern,
exported through db/services/index.ts, mirroring csm-portfolio/). It returns one typed area
envelope — the single fetch entry every other section in the epic extends rather than re-querying:
header: the SDM's display name + initials.area: the resolved assignment (regions, platforms) so sections can label "my area".leads[]: the in-scope leads (the demand side) the other sections aggregate over.experts[]: the in-scope experts (the supply side) the other sections aggregate over."mine" applies the area membership rule above; "tenant" returns the whole
tenant's leads + experts. Both are always tenant-isolated — an SDM can never see another tenant's
supply/demand.Wire the "my area" vs "all" toggle to the real query. The toggle drives the scope the service
resolves (mine vs tenant) so the rendered in-scope set is real and the toggle has an observable
effect (my area → only the SDM's regions/platforms; all → the tenant-wide set). The "all" side is
tenant-scoped, consistent with the mock showing multiple platforms/regions.
Retire the mock incrementally. Foundation wires the header (real SDM identity) and stands up the
area envelope / scope that the rest read from, and removes sdmName from apps/web/lib/mock/sdm.ts.
The demand, bid-readiness, risk, expert-strength, market-signals and service-performance section tables
stay on the mock this round — each is wired to the area envelope by its own feature (3–8). Render a
loading state while the area resolves and a defined empty state ("no area assigned yet") when the
SDM owns no regions or platforms — no crash, no mock fallback for the header.
sdm can be assigned an area (regions and/or platforms); the assignment is
stored on the sdm discriminator, indexed, and queryable per tenant./sdm (and /sdm/dashboard) is server-rendered and scoped to the signed-in SDM; the header shows
the real signed-in SDM, not the hard-coded Marcus Chen.sdmAreaService returns a single typed area envelope (header + area + in-scope leads[] +
experts[]) from @sustentus/services/server, scoped by tenant: scope: "mine" applies the area
membership rule (region in area.regions and platform in area.platforms, each constraining only
when set), scope: "tenant" widens to the tenant's full set — never another tenant's data.sdmName is removed from apps/web/lib/mock/sdm.ts; the still-mocked section tables (demand,
bid readiness, risks, expert strength, market signals, service performance) continue to render
unchanged pending their own features.sdm-supply-taxonomy feature (sequence 1, the dependency); this feature consumes the resolved platform
key, it does not define or seed the taxonomy.02_build/output/notes.mdfeat: sdm-area-foundation — SDM area assignment + scoped read-service + server dashboardpackages/services/src/db/models/user.ts: added an area sub-document
(regions: string[], platforms: ObjectId[] ref product) to the previously-empty
sdm discriminator, plus two { tenantId, "area.*" } indexes so SDMs are resolvable by the
supply they own. ISDMArea typed and exported.packages/services/src/db/services/sdm-area/{index.ts,instance.ts}: new sdmAreaService
(getAreaForSdm(tenantId, sdmUserId, scope)) returning the shared SdmAreaEnvelope
(header + area + in-scope leads[] + experts[]). scope: "mine" applies the area
membership rule — region in area.regions and platform in area.platforms, each
constraining only when that dimension is set; scope: "tenant" widens to the tenant. Both are
always tenant-scoped. Mirrors the csm-portfolio service shape.packages/services/src/db/services/index.ts: export the instance + the six public types so they
reach @sustentus/services/server.apps/web/app/(app)/sdm/dashboard/page.tsx: converted from a "use client" mock page to an
async server component — resolves the Clerk-authed SDM → tenant → app user (role === "sdm"),
reads the scope from searchParams, fetches the envelope, and renders the real signed-in SDM in
the header. /sdm/page.tsx re-exports it unchanged, so both routes are server-rendered.apps/web/components/dashboard/sdm/area-panel.tsx: new client-free panel with the "my area" vs
"all supply" toggle (links /sdm ↔ /sdm?scope=all) and the in-scope demand/expert tables, with
an inline empty state.apps/web/components/dashboard/sdm/empty-state.tsx: SdmDashboardEmpty for the unavailable case
(no tenant / not an SDM).apps/web/lib/mock/sdm.ts: removed sdmName; the six section tables still read the mock.sdm discriminator, indexed, queryable
per tenant — area + two { tenantId, "area.*" } indexes on SDMSchema./sdm (and /sdm/dashboard) server-rendered and scoped to the signed-in SDM; header shows the
real SDM, not Marcus Chen.sdmAreaService returns the typed envelope from @sustentus/services/server, scoped by
tenant: mine applies the region+platform rule, tenant widens — never another tenant's data.?scope=all),
with an observable difference in the in-scope set.mine; the toggle still widens to the tenant set.sdmName removed from apps/web/lib/mock/sdm.ts; the six section tables render unchanged.sdm-supply-taxonomy (sequence 1, not yet merged). That feature
introduces the two-level platform→module taxonomy and the lead/expert platform tagging. Until it
lands there is no platform parent, so area.platforms references the existing product taxonomy
(the modules: MM, Fusion, CRM…) and the platform filter matches lead.product / expert.products.
The membership rule degrades exactly as the spec intends: with no platforms assigned, the region
dimension alone scopes the area. When the taxonomy feature lands, the platform key re-points without
touching the envelope contract.03_release/output/changelog.mdYour go live readiness dashboard now knows who you are and which part of supply is yours:
03_release/output/investor-update.mdWho it's for: Service delivery managers (SDM) What shipped: The SDM go live readiness dashboard is now scoped to the signed-in SDM and shows the real leads and experts inside their area, with a toggle between their slice and the whole tenant. Why it matters: First real, per-SDM area ownership — groundwork for Build the Bridge, Q2 2026 Objective 3 (validate technical infrastructure & payout flow).
An SDM's area can now be owned and queried per tenant, the foundation every SDM supply and demand view builds on.
Dig deeper: https://github.com/sustentus/sustentus/pull/516 · https://help.sustentus.com/changelog/2026-06-23-sdm-area-foundation
03_release/output/release.mdmain as 086dd00 on 2026-06-23area field and the internal sdmAreaService are not described at field/service granularity in apps/docs/app/technical/**; no new app, route, env var, or architecture change (mirrors the csm-portfolio-foundation sibling).feature-role-matrix/service-journey; this wires real, area-scoped data behind it without changing documented persona capabilities. Area assignment is back-office (no UI) this round, so the source-of-truth pages don't lag.apps/help/app/changelog/2026-06-23-sdm-area-foundation/) + investor draft in this PRweb.sustentus.com/platform.sustentus.com) and help (help.sustentus.com) both green on the merge commit 086dd00.getAreaForSdm with scope: "mine" and an SDM who
has no assigned area collapsed both filters to { tenantId }, returning the whole tenant's
leads/experts under "my area" — violating spec AC #5 (no-area → empty state) and contradicting the
panel's "No area assigned" subtitle. Unlike the CSM sibling (whose mine is always pinned to
{ csm }), the SDM area has no identity pin, so an empty area must short-circuit. Added an
ownsNothing = scope === "mine" && !hasArea guard that returns empty leads/experts.findById not tenant-scoped (hardened on branch). Changed the SDM lookup to
findOne({ _id: sdmUserId, tenantId }) so the header/area can never resolve from a foreign-tenant
doc — defense in depth (the single caller already resolves the id under the tenant).hover:text-muted-foreground
duplicated the base colour; changed to hover:text-foreground for real hover affordance.lead.title || (lead.id ? lead.id : "—")
→ lead.title || lead.id || "—".fullName/initialsFor helpers (matches the per-service pattern across
csm-portfolio/sdm/expert-workbench); the 4-resolve/3-destructure Promise.all (registers the
product model for populate(), explained by a comment).area (regions/platforms), stored on the sdm discriminator, indexed,
queryable per tenant — area sub-doc + { tenantId, "area.regions" } / { tenantId, "area.platforms" } indexes./sdm (and /sdm/dashboard) server-rendered + scoped to the signed-in SDM; header shows the
real SDM, not Marcus Chen.sdmAreaService returns the typed envelope (header + area + leads[] + experts[]) from
@sustentus/services/server, tenant-scoped mine/tenant.ownsNothing guard
(no tenant-wide leak).sdmName removed from apps/web/lib/mock/sdm.ts; still-mocked section tables render unchanged.