# Cheat sheet — Platform Add-ons

The compressed essence of all seven topics. Skim the matching block after each lesson.

## Migrations (golang-migrate)
```
Add one:  ls migrations/{svc}/ | grep _migrate | sort -n | tail -1   # find last NNNN
          write migrations/{svc}/{NNNN+1}_migrate.up.sql             # 4 digits, literal "migrate"
          # new table → add resource_path + 2 RLS policies + ENABLE/FORCE RLS
          make gen-db-schema                                         # regenerate eureka.sql/dwh.sql for sqlc
Run:      ./local/run.bash start -m migration -s {svc}               # local  → gjob sql_migrate
          (prod: same gjob sql_migrate as a Helm hook job)
```
- **NO `.down.sql`** — the engine only calls `m.Up()`. Fix mistakes with a *new forward* migration.
- `eureka.sql`/`dwh.sql` are `pg_dump` outputs (sqlc schema); **never hand-edit** them.
- There is **no `make migrate`** — it's the binary's `gjob sql_migrate` subcommand.

## Linting & githooks
```
make init         # git config core.hooksPath .githooks   (one-time)
make lint         # golangci-lint run   (CI pins v2.8.0 — can differ from your PATH)
make lint-proto   # buf check lint (+ breaking vs develop)
```
- `.golangci.yaml`: `default: none` + explicit enable list; **`run.tests: false`** (tests unlinted);
  DB coverage = `bodyclose` + `rowserrcheck` (NOT upstream sqlclosecheck).
- **Custom `sqlclosecheck`** (`cmd/custom_lint/`): flags **`pgx.Rows`** not closed / closed without
  `defer`. **Self-test only** (`make test-sqlclosecheck-lint` vets `testdata/`); CI call is **commented out**.
- Githooks: `commit-msg` needs `LT-###` or `RMP-###` (1–2); `pre-commit` runs gitleaks (**skips if
  gitleaks absent**); `prepare-commit-msg` auto-prefixes `[LT-XXX]`. No `pre-push`.

## Secrets / SOPS / KMS
```
edit:     sops deployments/helm/backend/{svc}/secrets/{org}/{env}/{svc}.secrets.encrypted.yaml
```
- SOPS encrypts **values** (AES-256-GCM), keys stay readable; a `sops:` block holds the KMS-wrapped DEK.
- `.sops.yaml` (317 rules, `path_regex → gcp_kms/prod-{svc}`) is **generated — do not edit**.
- **Go services decrypt in-process at boot** (`configs.LoadAll` → `decrypt.File`) — handed the
  *encrypted* file, KMS via `GOOGLE_APPLICATION_CREDENTIALS`, secret has highest config precedence.
- Sidecar-decryption (`mozilla/sops`) is only for unleash/kafka/nats.
- **Second layer:** DB-stored private keys AES-encrypted by the app (`golibs/crypt`), keyed from config.

## Build / Skaffold
```
local build:  local/build-code-golang.bash   # GOOS=linux GOARCH=amd64 → build/{server,bdd.test,stub}
k8s (kind):   deployments/sk.bash             # skaffold.local.yaml → custom build → kind load → Helm
compose:      ./local/run.bash                # separate path; hot-swaps build/server into a container
```
- **One binary, all services**; runtime picks the service via cobra subcommand (`gserver`/`gjob`) +
  mounted config. Kubernetes runs the same image many times.
- Dev image = `development.Dockerfile:developer` (debian, **copies** prebuilt binary); prod =
  `release.Dockerfile:runner` (alpine, **compiles** inside).
- Skaffold files at **repo root**; `build.artifacts` live inside **profiles** (custom buildCommand).

## Reliability (idempotency · retries · outbox)
```
handler contract:  func(ctx, data) (retryable bool, err error)   # true = redeliver / retry
retry helper:      try.Do / try.DoBackOff   (caps at 10)
idempotency:       IdempotentService.GetOne(id, type) → exists? drop : process; Insert marker AFTER success
```
- **Idempotent consumer:** record processed IDs, discard dupes (broker = at-least-once). Repo's
  `idempotent` table is in the **bob** DB; marker written only on success/non-retryable.
- **Retries:** Kafka = **in-process** loop first (strict consumer leaves offset → broker redelivers;
  loose/AlwaysCommit commits anyway); NATS = don't-Ack → redeliver after `AckWait`, up to `MaxDeliver`.
- **Transactional outbox** (Chris Richardson): INSERT the event in the **same tx** as the write; a relay
  ships it → solves the dual-write problem. Here: **eureka-only**, relayed by **Debezium**, feature-flagged.
- **spike/notification** don't use it — **publish-after-commit** + `*_FAILED` status + caller retry +
  idempotency. They accept the dual-write risk on purpose.

## Email (spike + SendGrid)
```
CREATED ──publish──▶ QUEUED ──consume+SendGrid──▶ PROCESSED ──webhook──▶ email_recipient_events
  (tx)               (Kafka)      (send)            (sg_message_id)        (delivered/bounce/…)
```
- Priority lane (topic) from `client_id`; SendGrid client (trans vs noti) from `EmailType` — **orthogonal**.
- Both priority consumers run in **one pod** (diff consumer groups). `QUEUED` ≠ sent.
- Retryable send failure retries; `ErrPayloadTooLarge` and post-send DB failures are **non-retryable**
  (avoid double-send).

## Push (notification + FCM)
```
Notification.Created (NATS) / AsyncNotificationUpserting (Kafka)
  → ProcessPushNotification → find device tokens → NotificationPusher.PushMultiple (≤500) → FCM
```
- FCM wrapped in **golibs/firebase** (`PushMultiple` chunks of 500; per-token BatchResponse).
- Unregistered tokens → cleanup topic (not a failure); push failures **swallowed** (non-retryable).
- conversationmgmt publishes `Notification.Created`; notification **requests emails via spike gRPC**.

## Cron / scheduled jobs
```
define:  bootstrap.RegisterJob("svc_job_name", jobFunc)   # in init()  → cobra subcommand
run:     ./local/run.bash cron -n <name>                  # local
sched:   Helm values cronjobs: { name: { cmd, schedule: '*/1 * * * *' } }   # k8s CronJob
```
- **No in-process scheduler** — k8s CronJob drives the timer; the Go entrypoint runs once and exits.
- `jobs:` (run-once) vs `cronjobs:` (recurring), same `RegisterJob` mechanism.
- **gjob consumer pod** = long-running job, **KEDA-scaled on Kafka lag** (Prometheus), not CPU.
- Three shapes, one binary: in-gRPC-pod consumers · KEDA gjob pod · CronJobs.

## Interview one-liners
- **Migrations?** "Ordered forward-only SQL via golang-migrate; no down files — we fix forward. New
  tables carry RLS. A generated schema dump feeds sqlc."
- **Outbox?** "Write the event in the same tx as the state change; a relay (Debezium CDC for us) ships
  it — no dual-write. We use it in one service; elsewhere we publish-after-commit with status
  compensation and idempotent consumers."
- **Idempotent consumer?** "Brokers are at-least-once, so we record processed message IDs and drop
  dupes — writing the marker only after success so a retry re-runs."
- **Secrets?** "SOPS encrypts values, GCP KMS wraps the data key; services decrypt in-process at boot,
  no plaintext on disk. DB-stored keys get a second app-level AES layer."
- **Build?** "One binary, all services, chosen by a subcommand; cross-compiled; Skaffold+kind for k8s,
  docker-compose for the fast local loop."
