pkg/ — Shared Infrastructure Clients
pkg/ holds reusable clients for talking to external systems — things not owned by any single domain and safe to import from anywhere in CoreAPI.
This is not where business logic lives. If it knows about your Postgres models, your domain rules, or your own response envelope (pkg/response), it belongs in a domain/, not here.
What belongs here vs. what doesn't
Belongs in pkg/<name>Client |
Belongs in domain/ |
|---|---|
| HTTP calls to an external system (Castis Streamer, cproxy, ELB) | Deciding which node to call |
| Translating your request into that vendor's expected JSON shape | Your own DB reads/writes |
| Parsing that vendor's response shape | Building pkg/response envelopes for your frontend |
Vendor-specific ID/slug conventions (e.g. .stream suffix) |
Orchestrating multiple client calls into one business flow |
A client in pkg/ should be usable by someone who has never seen your database schema. It takes plain parameters (or a small DB-derived struct like service.Streamer) and returns plain Go values — never a Fiber context, never your envelope.
Standard shape
Every client package follows the same two-file split, regardless of what it talks to:
pkg/<Name>Client/
├── client.go — one function per remote operation, plus private HTTP helpers
└── models.go — request/response shapes for THIS vendor's API only
client.go contains:
- One exported function per operation (CreateStream, PurgeContent, GetStats — verbs, not CRUD-generic names)
- Private helpers for the mechanics: doGet, doPost, readResponse
- Any vendor-shape translation the caller shouldn't have to know about (payload building, ID formatting)
models.go contains:
- Only the shapes needed to talk to the vendor — request bodies, response envelopes as that vendor defines them
- Never your GORM models. Never pkg/response types.
The stateless-client rule
Clients don't hold a stored target. Every function takes the node to hit as a parameter:
func CreateStream(node service.Streamer, payload json.RawMessage) (*StreamerResponse, error)
func GetStats(node service.Streamer) (*StreamerResponse, error)
Why: a client with a single baked-in baseURL can't serve two nodes. Since we run multiple Streamer/cproxy/ELB nodes, the caller (a domain handler or service) looks up the target row in Postgres and passes it in. The client stays a pure function of its inputs — easy to test, easy to point at a different node without touching the client at all.
Error handling
Return plain error — don't wrap in pkg/response. That envelope is for responses going back to a browser; a pkg/ client is calling out, not responding to anyone. Let the calling domain handler decide how a client error becomes a response.Error(...) for its own consumers.
Naming convention
Package + folder name is <Vendor><Purpose>Client — e.g. StreamerClient, CProxyClient, ELBClient. Keep the Client suffix even though the folder already implies it; it disambiguates from a hypothetical domain/service registry of the same vendor's nodes.
Adding a new client — checklist
- Create
pkg/<Name>Client/client.goandmodels.go - Write one function per operation you need right now — don't pre-build the whole vendor API surface
- Every function signature takes the target (host/port/whatever identifies the node) as its first parameter
- Vendor-specific payload shaping lives inside the client, not in the domain calling it
- Return
(result, error)— no Fiber, no envelope - Wire the domain caller to look up the target row from Postgres and pass it in
- Document it under
pkg/<name>/in these docs —index.md(what it does, function reference) +changelog.md(dated entries, same format as domain changelogs)
Reference implementations
- StreamerClient — Castis Streamer node control (create/delete streams, playlist payloads)
- CProxyClient — cproxy origin/cache control (origin switching, purge)
Both are the pattern to copy from when adding a new client (e.g. a payment gateway SDK wrapper). ELBClient exists in code but isn't documented yet — same shape applies.
Not every pkg/ package is a vendor client
A couple of shared packages don't fit the client pattern above — they're documented separately rather than forced into this table:
- pkg/seed — startup seed data, two different reseed-on-boot conventions in use, plus a hard-reset companion for heavier schema churn
pkg/db— connection setup andAutoMigratewiring, see Database and Migrations