# Member Service Dev

> 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.

- Skill: `linuxfoundation/member-service-dev` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add linuxfoundation/member-service-dev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/linuxfoundation/member-service-dev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: linuxfoundation (https://skillmd.com/u/linuxfoundation)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/linuxfoundation/member-service-dev

---


<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

# 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:

1. **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 `uid`s 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.
2. **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`.

