| name | member-service-dev |
| description | 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. |
| paths | ["**/*.go","go.mod","go.sum","Makefile","cmd/**","internal/**","pkg/**","gen/**",".claude/skills/member-service-dev/**"] |
| allowed-tools | Read, Glob, Grep, Edit, Write, Bash |
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.