Development Conventions
Repo-local Go and Goa conventions for the LFX V2 Member Service. This skill
attaches whenever Go or service files are in scope. Central skills explain
service classification and platform shape; this skill drives implementation
work here.
Scope and handoffs
- This skill: Go coding conventions, Goa boundaries, NATS KV cache and RPC
conventions, tests, formatting, license headers, plus the Salesforce
callout below.
lfx-skills:lfx-platform-architecture: V2 service classes,
write/read/access-check flows, NATS/KV ownership, cross-repo handoffs.
lfx-skills:lfx: cross-repo topology and routing.
- Local
member-add-endpoint skill (in this repo): the step-by-step recipe
for adding or changing a membership HTTP endpoint (Goa design, regen,
handler, tests, Heimdall ruleset). Do not duplicate that recipe here.
This service publishes indexer and FGA-sync messages
Member service is a Salesforce-backed read/write proxy that also publishes to
the platform indexer and FGA-sync on the write path. It reads through
SOQL-backed and sObject caches; key-contact and b2b-org mutations write
Salesforce or the org-settings KV bucket, then publish downstream events.
Subjects live in pkg/constants/subjects.go:
- Indexer:
lfx.index.b2b_org, lfx.index.b2b_org_settings,
lfx.index.project_membership, lfx.index.key_contact.
- FGA-sync:
lfx.fga-sync.update_access, lfx.fga-sync.delete_access; the
key-contact relation grants/revokes also publish lfx.fga-sync.member_put /
lfx.fga-sync.member_remove via the lfx-v2-fga-sync package constants.
Publishing goes through the port.MemberPublisher port
(internal/domain/port/event_publisher.go), implemented by
internal/infrastructure/nats/publisher.go. Write-path policy: creates and
updates publish fire-and-forget (sync=false) and swallow publish errors,
logging at warn with publish_failed_for_backfill_repair=true so the
POST /admin/reindex backfill can recover; deletes propagate publish errors.
When a settings PUT publishes, FGA-sync is sent before the indexer so access
tuples land before the doc is searchable. The same publish helpers are reused
by the Salesforce Pub/Sub CDC consumer (internal/service/cdc_consumer.go,
run as a separate single-replica Deployment with RUN_MODE=consumer) and the
POST /admin/reindex backfill runner. The canonical contracts live in
docs/fga-contract.md and docs/indexer-contract.md (and upstream in
lfx-v2-fga-sync and lfx-v2-indexer-service); update those in the same
change when message shapes change. For the current-vs-target architecture and
the graduation plan, see ARCHITECTURE.md at the repo root.
Generated code
- Do not edit anything under
gen/ by hand. Files are regenerated by
make apigen.
- Change Goa design under
cmd/member-api/design/ first, then regenerate.
- Hand-written implementation lives under
cmd/, internal/, and pkg/.
Match the existing package layout and constructor style before adding a
new abstraction.
Logging
- Use Go's standard
log/slog package or this repo's existing slog
wrapper. No fmt.Println, fmt.Printf, log.Print*, or log.Println
for runtime logging.
- Include stable structured fields when available:
request_id,
principal, object_type, object_id, action, operation.
- Never log tokens, secrets, private keys, raw bearer headers, Salesforce
credentials, or raw payloads that may contain PII.
- Honor
LOG_LEVEL and LOG_ADD_SOURCE.
Errors
- Use the existing domain error types from
pkg/errors: Validation,
NotFound, Conflict, ServiceUnavailable, PreconditionFailed,
NotImplemented, and Unexpected. Mapping to HTTP status lives in
cmd/member-api/service/error.go.
- Current
wrapError mapping: not found 404, validation 400, conflict 409,
unavailable 503, precondition failed 412, not implemented 501, and anything
else 500. The matching Goa dsl.Error/dsl.Response declarations must
exist on a method before its handler can return that status; if a new
endpoint needs a status not yet declared, update the Goa design and
wrapError in the same change.
- Wrap upstream errors so
errors.Is and errors.Unwrap still work.
- Translate at the Goa or transport boundary. Do not return raw Salesforce
HTTP errors, raw NATS errors, or upstream provider payloads to clients.
- Do not introduce a parallel sentinel-error family.
Request context
- Middleware owns request context setup. Service-layer code should not read
HTTP headers directly.
- Propagate
request_id, principal, and inbound authorization through the
repo's context helpers and pkg/constants keys
(constants.PrincipalContextID, etc). No bare string context keys.
- Forward context values into NATS messages or downstream calls only when
the receiving contract needs them.
Pagination
The current resource-rooted HTTP surface has no list endpoints; pagination
lives on the internal membership read path (ListMembershipsForProject and
the SOQL batch cache) and in the design's ListMetadata type. If a list
endpoint is (re)added:
- HTTP query params are Goa camelCase:
pageSize plus opaque pageToken.
Responses expose metadata.next_page_token.
- Normalize
pageSize to supported Salesforce batch/page sizes.
- Return
metadata.next_page_token only when another page exists.
- Treat
pageToken as opaque and service-owned. Clients must not parse it.
NATS and KV
- Keep subject strings in repo-owned constants (see
pkg/constants and
internal/infrastructure/nats/). Do not hardcode subject strings at call
sites.
- The inbound RPC handlers are registered with NATS
QueueSubscribe (queue group
constants.ServiceName, i.e. lfx-v2-member-service) and drained during shutdown:
- project-id-map lookup (
lfx.member.project-id-map.lookup)
- b2b_org lookup (
lfx.member.b2b_org_lookup)
The earlier SFID/UUID lookup subjects were removed in LFXV2-2049 (the
canonical uid is now the 18-char SFID).
- If adding another horizontally scaled request/reply handler, use the same
queue group (
constants.ServiceName) and document it in
references/nats-messaging.md.
- Do not write directly to another service's KV bucket. This repo owns
membership-cache, member-service-cache, org-settings, and
pubsub-state (CDC replay cursors).
- The existing shutdown path drains the inbound RPC subscriptions and closes
the shared NATS client; match that pattern unless you are deliberately
changing shutdown semantics.
- When subjects, queue groups, payload shapes, or KV buckets change, update
references/nats-messaging.md in the same change.
Goa boundaries (this repo)
- Design files live in
cmd/member-api/design/.
- Resource-rooted API surface. Single-object reads, writes, and an admin
reindex action:
b2b_org: GET/POST/PUT /b2b_orgs[/{uid}], plus GET/PUT
/b2b_orgs/{uid}/settings and the per-principal settings-user endpoints
POST /b2b_orgs/{uid}/settings/users and PUT/DELETE
/b2b_orgs/{uid}/settings/users/{email}.
project_membership: GET /project_memberships/{uid} only (membership
lifecycle is owned by Salesforce, not this HTTP API).
key_contact: GET/POST/PUT/DELETE nested under
/project_memberships/{membership_uid}/key_contacts[/{uid}].
POST /admin/reindex triggers an indexer/FGA backfill.
- The canonical
uid for Salesforce-backed entities is the 18-char SFID
(LFXV2-2049); pkg/sfuuid only normalizes 15↔18-char SFID forms
(Normalize18/Normalize15). Project UIDs remain real v2 UUIDs. UID, slug,
and SFID translation is done in the infrastructure layer via NATS RPC,
Salesforce, and pkg/sfuuid, never inside Goa generated code.
- Mutating endpoints implement optimistic concurrency. PUT/DELETE accept an
If-Match header carrying the LFX ETag from a prior GET; a stale ETag
returns 412 Precondition Failed. GET responses carry an ETag (a hash of
the serialised domain object) and Last-Modified, and accept
If-None-Match/If-Modified-Since. Mirror the established
UpdateKeyContact guard when adding a new mutating handler; settings PUT
uses an org-settings KV revision for compare-and-set and can return 409 Conflict.
- For the full add-endpoint recipe (design, regen, handler, tests,
Heimdall ruleset), use the local
member-add-endpoint skill.
Salesforce integration callout
Salesforce REST is the source of truth for tiers, memberships, and key
contacts. Two concrete rules sit on top of the general Go conventions:
- Always go through the resolver and cache. Project-scoped SOQL queries
require a Salesforce
Project__c.Id. Project identifiers on the HTTP API
are v2 UUIDs (entity uids are 18-char SFIDs since LFXV2-2049). Use
ProjectResolver.SFIDFromUID (which in turn checks the
membership-cache KV bucket, then NATS RPC to project-service, then
SOQL). Never issue SOQL keyed on a v2 UUID directly.
- Respect the cache freshness contract.
CacheStatusFresh serves
immediately. CacheStatusStale serves and triggers a background refresh.
CacheStatusExpired and CacheStatusMiss fetch synchronously from
Salesforce. New cached endpoints must honor the same four states defined
in internal/infrastructure/nats/cache.go.
Cache layout, TTLs, and resolver chains live in
docs/agent-guidance/salesforce-cache.md. Auth flows and env vars live in
docs/agent-guidance/salesforce-integration.md. Do not duplicate those
contracts here.
Tests
- Depend on interfaces for external systems: Salesforce repositories, NATS
RPC clients, KV cache, JWT authenticator.
- Keep data mocks in
internal/infrastructure/mock/. Existing service tests
usually use auth.NewJWTAuth(auth.JWTAuthConfig{MockLocalPrincipal: ...});
auth.MockJWTAuth is available when a test needs explicit authenticator
expectations.
- Table-driven tests for branching behavior.
- Exactly one test function per exported method, with multiple cases in the
table.
- Co-locate
*_test.go with the code under test.
- Run
make test before handing off when code changed. This repo's make test already uses go test -v -race -coverprofile=coverage.out ./....
Formatting, lint, license
- Run
make fmt before Go handoff when code changed. Run make lint when
lint signal is needed. There is no make check target in this repo today.
- Preserve the standard MIT license header on every new Go and YAML file.
- Document exported Go symbols when the repo lint requires it. Add
implementation comments only where the code is not self-explanatory.
- Update repo-owned docs or contracts in the same change as code that
changes behavior.
References
references/nats-messaging.md: repo-local NATS subjects, RPC payload
shapes, and KV bucket inventory. Read when adding, changing, or consuming a
NATS subject or KV bucket in this service.
references/development-workflow.md: prerequisites, Goa code generation,
build/test/run targets, testing patterns, JWT and Heimdall flow, OpenFGA
project type, Docker, CI/CD, and error mapping. Read for workflow context.
The endpoint procedural recipe lives in the local member-add-endpoint
skill.
For platform composition, V2 service classes, write/read/access-check
flows, and cross-repo handoffs, use lfx-skills:lfx-platform-architecture.
For cross-repo routing or "where does X live", use lfx-skills:lfx.
1---2name: member-service-dev3description: Path-scoped Go and Goa conventions for the lfx-v2-member-service repo. Auto-attaches when editing Go implementation, Goa design files, NATS KV cache code or request/reply handlers, logging, pagination, errors, request context, generated-code boundaries, tests, lint, formatting, or license headers. Inline guidance covers conventions every Go file in this repo follows, plus a Salesforce-integration callout. References cover repo-local NATS/KV contracts and the broader development workflow. Central platform composition stays in lfx-skills:lfx-platform-architecture; the membership endpoint workflow stays in the local member-add-endpoint skill.4---56<!-- Copyright The Linux Foundation and each contributor to LFX. -->7<!-- SPDX-License-Identifier: MIT -->89# Development Conventions1011Repo-local Go and Goa conventions for the LFX V2 Member Service. This skill12attaches whenever Go or service files are in scope. Central skills explain13service classification and platform shape; this skill drives implementation14work here.1516## Scope and handoffs1718- This skill: Go coding conventions, Goa boundaries, NATS KV cache and RPC19 conventions, tests, formatting, license headers, plus the Salesforce20 callout below.21- `lfx-skills:lfx-platform-architecture`: V2 service classes,22 write/read/access-check flows, NATS/KV ownership, cross-repo handoffs.23- `lfx-skills:lfx`: cross-repo topology and routing.24- Local `member-add-endpoint` skill (in this repo): the step-by-step recipe25 for adding or changing a membership HTTP endpoint (Goa design, regen,26 handler, tests, Heimdall ruleset). Do not duplicate that recipe here.2728## This service publishes indexer and FGA-sync messages2930Member service is a Salesforce-backed read/write proxy that also publishes to31the platform indexer and FGA-sync on the write path. It reads through32SOQL-backed and sObject caches; key-contact and b2b-org mutations write33Salesforce or the `org-settings` KV bucket, then publish downstream events.34Subjects live in `pkg/constants/subjects.go`:3536- Indexer: `lfx.index.b2b_org`, `lfx.index.b2b_org_settings`,37 `lfx.index.project_membership`, `lfx.index.key_contact`.38- FGA-sync: `lfx.fga-sync.update_access`, `lfx.fga-sync.delete_access`; the39 key-contact relation grants/revokes also publish `lfx.fga-sync.member_put` /40 `lfx.fga-sync.member_remove` via the `lfx-v2-fga-sync` package constants.4142Publishing goes through the `port.MemberPublisher` port43(`internal/domain/port/event_publisher.go`), implemented by44`internal/infrastructure/nats/publisher.go`. Write-path policy: creates and45updates publish fire-and-forget (`sync=false`) and swallow publish errors,46logging at warn with `publish_failed_for_backfill_repair=true` so the47`POST /admin/reindex` backfill can recover; deletes propagate publish errors.48When a settings PUT publishes, FGA-sync is sent before the indexer so access49tuples land before the doc is searchable. The same publish helpers are reused50by the Salesforce Pub/Sub CDC consumer (`internal/service/cdc_consumer.go`,51run as a separate single-replica Deployment with `RUN_MODE=consumer`) and the52`POST /admin/reindex` backfill runner. The canonical contracts live in53`docs/fga-contract.md` and `docs/indexer-contract.md` (and upstream in54`lfx-v2-fga-sync` and `lfx-v2-indexer-service`); update those in the same55change when message shapes change. For the current-vs-target architecture and56the graduation plan, see `ARCHITECTURE.md` at the repo root.5758## Generated code5960- Do not edit anything under `gen/` by hand. Files are regenerated by61 `make apigen`.62- Change Goa design under `cmd/member-api/design/` first, then regenerate.63- Hand-written implementation lives under `cmd/`, `internal/`, and `pkg/`.64 Match the existing package layout and constructor style before adding a65 new abstraction.6667## Logging6869- Use Go's standard `log/slog` package or this repo's existing `slog`70 wrapper. No `fmt.Println`, `fmt.Printf`, `log.Print*`, or `log.Println`71 for runtime logging.72- Include stable structured fields when available: `request_id`,73 `principal`, `object_type`, `object_id`, `action`, `operation`.74- Never log tokens, secrets, private keys, raw bearer headers, Salesforce75 credentials, or raw payloads that may contain PII.76- Honor `LOG_LEVEL` and `LOG_ADD_SOURCE`.7778## Errors7980- Use the existing domain error types from `pkg/errors`: `Validation`,81 `NotFound`, `Conflict`, `ServiceUnavailable`, `PreconditionFailed`,82 `NotImplemented`, and `Unexpected`. Mapping to HTTP status lives in83 `cmd/member-api/service/error.go`.84- Current `wrapError` mapping: not found 404, validation 400, conflict 409,85 unavailable 503, precondition failed 412, not implemented 501, and anything86 else 500. The matching Goa `dsl.Error`/`dsl.Response` declarations must87 exist on a method before its handler can return that status; if a new88 endpoint needs a status not yet declared, update the Goa design and89 `wrapError` in the same change.90- Wrap upstream errors so `errors.Is` and `errors.Unwrap` still work.91- Translate at the Goa or transport boundary. Do not return raw Salesforce92 HTTP errors, raw NATS errors, or upstream provider payloads to clients.93- Do not introduce a parallel sentinel-error family.9495## Request context9697- Middleware owns request context setup. Service-layer code should not read98 HTTP headers directly.99- Propagate `request_id`, `principal`, and inbound authorization through the100 repo's context helpers and `pkg/constants` keys101 (`constants.PrincipalContextID`, etc). No bare string context keys.102- Forward context values into NATS messages or downstream calls only when103 the receiving contract needs them.104105## Pagination106107The current resource-rooted HTTP surface has no list endpoints; pagination108lives on the internal membership read path (`ListMembershipsForProject` and109the SOQL batch cache) and in the design's `ListMetadata` type. If a list110endpoint is (re)added:111112- HTTP query params are Goa camelCase: `pageSize` plus opaque `pageToken`.113 Responses expose `metadata.next_page_token`.114- Normalize `pageSize` to supported Salesforce batch/page sizes.115- Return `metadata.next_page_token` only when another page exists.116- Treat `pageToken` as opaque and service-owned. Clients must not parse it.117118## NATS and KV119120- Keep subject strings in repo-owned constants (see `pkg/constants` and121 `internal/infrastructure/nats/`). Do not hardcode subject strings at call122 sites.123- The inbound RPC handlers are registered with NATS `QueueSubscribe` (queue group124 `constants.ServiceName`, i.e. `lfx-v2-member-service`) and drained during shutdown:125 - project-id-map lookup (`lfx.member.project-id-map.lookup`)126 - b2b_org lookup (`lfx.member.b2b_org_lookup`)127 The earlier SFID/UUID lookup subjects were removed in LFXV2-2049 (the128 canonical uid is now the 18-char SFID).129- If adding another horizontally scaled request/reply handler, use the same130 queue group (`constants.ServiceName`) and document it in131 `references/nats-messaging.md`.132- Do not write directly to another service's KV bucket. This repo owns133 `membership-cache`, `member-service-cache`, `org-settings`, and134 `pubsub-state` (CDC replay cursors).135- The existing shutdown path drains the inbound RPC subscriptions and closes136 the shared NATS client; match that pattern unless you are deliberately137 changing shutdown semantics.138- When subjects, queue groups, payload shapes, or KV buckets change, update139 `references/nats-messaging.md` in the same change.140141## Goa boundaries (this repo)142143- Design files live in `cmd/member-api/design/`.144- Resource-rooted API surface. Single-object reads, writes, and an admin145 reindex action:146 - `b2b_org`: GET/POST/PUT `/b2b_orgs[/{uid}]`, plus GET/PUT147 `/b2b_orgs/{uid}/settings` and the per-principal settings-user endpoints148 POST `/b2b_orgs/{uid}/settings/users` and PUT/DELETE149 `/b2b_orgs/{uid}/settings/users/{email}`.150 - `project_membership`: GET `/project_memberships/{uid}` only (membership151 lifecycle is owned by Salesforce, not this HTTP API).152 - `key_contact`: GET/POST/PUT/DELETE nested under153 `/project_memberships/{membership_uid}/key_contacts[/{uid}]`.154 - `POST /admin/reindex` triggers an indexer/FGA backfill.155- The canonical `uid` for Salesforce-backed entities is the 18-char SFID156 (LFXV2-2049); `pkg/sfuuid` only normalizes 15↔18-char SFID forms157 (`Normalize18`/`Normalize15`). Project UIDs remain real v2 UUIDs. UID, slug,158 and SFID translation is done in the infrastructure layer via NATS RPC,159 Salesforce, and `pkg/sfuuid`, never inside Goa generated code.160- Mutating endpoints implement optimistic concurrency. PUT/DELETE accept an161 `If-Match` header carrying the LFX ETag from a prior GET; a stale ETag162 returns `412 Precondition Failed`. GET responses carry an `ETag` (a hash of163 the serialised domain object) and `Last-Modified`, and accept164 `If-None-Match`/`If-Modified-Since`. Mirror the established165 `UpdateKeyContact` guard when adding a new mutating handler; settings PUT166 uses an `org-settings` KV revision for compare-and-set and can return `409167 Conflict`.168- For the full add-endpoint recipe (design, regen, handler, tests,169 Heimdall ruleset), use the local `member-add-endpoint` skill.170171## Salesforce integration callout172173Salesforce REST is the source of truth for tiers, memberships, and key174contacts. Two concrete rules sit on top of the general Go conventions:1751761. **Always go through the resolver and cache.** Project-scoped SOQL queries177 require a Salesforce `Project__c.Id`. Project identifiers on the HTTP API178 are v2 UUIDs (entity `uid`s are 18-char SFIDs since LFXV2-2049). Use179 `ProjectResolver.SFIDFromUID` (which in turn checks the180 `membership-cache` KV bucket, then NATS RPC to project-service, then181 SOQL). Never issue SOQL keyed on a v2 UUID directly.1822. **Respect the cache freshness contract.** `CacheStatusFresh` serves183 immediately. `CacheStatusStale` serves and triggers a background refresh.184 `CacheStatusExpired` and `CacheStatusMiss` fetch synchronously from185 Salesforce. New cached endpoints must honor the same four states defined186 in `internal/infrastructure/nats/cache.go`.187188Cache layout, TTLs, and resolver chains live in189`docs/agent-guidance/salesforce-cache.md`. Auth flows and env vars live in190`docs/agent-guidance/salesforce-integration.md`. Do not duplicate those191contracts here.192193## Tests194195- Depend on interfaces for external systems: Salesforce repositories, NATS196 RPC clients, KV cache, JWT authenticator.197- Keep data mocks in `internal/infrastructure/mock/`. Existing service tests198 usually use `auth.NewJWTAuth(auth.JWTAuthConfig{MockLocalPrincipal: ...})`;199 `auth.MockJWTAuth` is available when a test needs explicit authenticator200 expectations.201- Table-driven tests for branching behavior.202- Exactly one test function per exported method, with multiple cases in the203 table.204- Co-locate `*_test.go` with the code under test.205- Run `make test` before handing off when code changed. This repo's `make206 test` already uses `go test -v -race -coverprofile=coverage.out ./...`.207208## Formatting, lint, license209210- Run `make fmt` before Go handoff when code changed. Run `make lint` when211 lint signal is needed. There is no `make check` target in this repo today.212- Preserve the standard MIT license header on every new Go and YAML file.213- Document exported Go symbols when the repo lint requires it. Add214 implementation comments only where the code is not self-explanatory.215- Update repo-owned docs or contracts in the same change as code that216 changes behavior.217218## References219220- `references/nats-messaging.md`: repo-local NATS subjects, RPC payload221 shapes, and KV bucket inventory. Read when adding, changing, or consuming a222 NATS subject or KV bucket in this service.223- `references/development-workflow.md`: prerequisites, Goa code generation,224 build/test/run targets, testing patterns, JWT and Heimdall flow, OpenFGA225 `project` type, Docker, CI/CD, and error mapping. Read for workflow context.226 The endpoint procedural recipe lives in the local `member-add-endpoint`227 skill.228229For platform composition, V2 service classes, write/read/access-check230flows, and cross-repo handoffs, use `lfx-skills:lfx-platform-architecture`.231For cross-repo routing or "where does X live", use `lfx-skills:lfx`.