Domain language used by the 7Cav API. The proto files
(proto/milpacs.proto, proto/tickets.proto) are the contract; this
document covers the concepts and any nuance that isn't obvious from
reading the schema.
The API is a read layer over a XenForo forum's
MySQL database, augmented by the NF Rosters add-on (which contributes
the xf_nf_rosters_* tables that hold milpac records) and the
Cav7/ApiKeyManager add-on (which contributes the API-key and scope
tables). The API itself owns no schema; it queries upstream tables and
maps them to its own proto types.
A "milpac" is a member's military-personnel-record entry: rank,
position, awards, service record, and the identifiers used to look them
up across the wider 7Cav stack. MilpacService is the surface that
serves them.
A member's milpac is served in three shapes by different RPCs, each tuned to a known consumer:
Profile— full view: rank, positions, awards, records, and the connected-account identifiers.LiteProfile— slim view: enough to render a roster row, without the per-member relational payload.S1UniformsProfile— view for the uniforms tool; includes uniform-relevant fields and omits the rest.
The three are not subsets of one type; they are hand-mapped from the same upstream rows into distinct proto messages.
A Roster is a collection of members grouped by unit, course, or
status. RosterType is an enum identifying which roster is requested;
its numeric values are used as the roster_id foreign key in the
upstream tables. The three roster RPCs (GetRoster, GetLiteRoster,
GetS1UniformsRoster) return the same set of members in the
corresponding profile shape above.
A Rank is a pay-grade entry from the upstream rank catalog. A
Position is an org-chart slot; positions are grouped into
PositionGroups for hierarchical browsing. RankExpanded and
PositionExpanded are the variants that include relational fields the
plain message omits.
A Record is an entry on a member's service history (joins, promotions,
transfers, etc.); RecordType enumerates the categories. An Award is
a decoration entry.
An entry on the AWOL list — members flagged as absent without leave.
Served by GetAwol, used by status-tracking consumers.
Members are looked up by 7Cav user id, by username, and by external account identifiers maintained by the forum's connected-account integrations:
- Discord — Discord user id.
- Gamertag — Xbox / PlayStation handle.
- Keycloak — legacy SSO identifier. The Keycloak auth path has been removed; the lookup RPC is on the chopping block and should not be used in new code.
TicketsService exposes the forum's ticket system (powered by the
NF Tickets add-on) as a read-only API.
Ticket— a thread: title, status, category, participants, message count, timestamps.forum_urlis populated when the API is configured with the public forum base URL.Message— one post within a ticket, addressed byposition(0-indexed within the thread).Category— a top-level grouping for tickets; carries a current ticket count.TicketParticipant— a member-to-ticket association with a role.
ListTicketMessages paginates with an opaque cursor whose semantic is
"next position to include" (inclusive lower bound), so position=0
is reachable.
Clients authenticate with a Bearer token (case-insensitive prefix per
RFC 7235). Tokens are issued by the forum admin UI, not by this
service. Each token carries a set of named scopes. Current scopes:
read— gates the milpac surface (profiles, rosters, ranks, positions, AWOL).read:tickets— gates the tickets surface.
Scope membership is checked per-handler; a token with read cannot
read tickets, and vice versa.
testdb/ is the dockerized MariaDB harness — the "SQL seam" from PRD
#112's testing decisions. testdb.Open(t) hands a test its own
disposable database (forum-shaped schema + fixtures, embedded in the
package) on a MariaDB 11.5 server; tests opt in via TESTDB_ADDR and
skip without it. Run locally with make test-integration.
Two properties of the harness are load-bearing:
- The schema deliberately omits the four indexes PRD #112 proposes, so "red" EXPLAIN plans stay reproducible (each test may CREATE INDEX in its own database to observe the flip).
- The fixtures include a member whose milpac
relation_idcollides with another member's forumuser_id(205), keeping the by-id profile route's frozen relation-key semantic testable.
testdb/indexes.sql is the in-repo source of truth for the four
indexes backing the hot read paths (composite user_id_post_date on
xf_post serving two distinct aggregations — a loose index scan for
the last-post aggregation and a covering index scan for the AWOL
report's variant, whose extra MAX(post_id) disqualifies the loose
scan; relation-id and user-id indexes on the rosters tables for the
profile preloads). The EXPLAIN-plan tests in testdb/indexes_test.go
pin each flip red→green: the unindexed schema must full-scan, the
script must produce the loose scan, the covering scan, and
index-backed preloads — a query or schema change that silently
reintroduces a full scan fails a test, not a production latency
budget.
The API never executes DDL. The script is applied manually by the DB
admin (mysql xenforo < testdb/indexes.sql, human-gated in #122) and
re-applied with the same one command after any forum add-on upgrade
that rebuilds the tables (idempotent: ADD INDEX IF NOT EXISTS). The
long-term home for re-application is the ApiKeyManager add-on's schema
step (per PRD #112) — documented intent only, not implemented.