7th Cavalry Apps (CavApps) is a Nextjs based collection of tools and apps designed to aid the 7th Cavalry Gaming Regiment in its day to day functions. It currently includes the Active Duty Roster (ADR), a small collection of Roster Statistics, and the Uniform Builder. Future iterations could include a more advanced statistics tool, an AWOL tracker, and a migration of S1 Documents, among other possible tools. CavApps uses a Frontend-Backend architecture and includes basic authentication.
The live deployment can be found at https://apps.7cav.us/ and the backend at https://bff.apps.7cav.us/
NOTE: This documentation is written so that an average member of the 7th Cavalry should be able to make basic edits to CavApps. If you need help with a particular matter or believe this documentation could be improved, please message S6 Development Staff on Discord or on the Forums.
CavApps is a monorepo with two parts:
server/: an Express caching proxy ("BFF") that fetches roster data from the 7th Cavalry API and serves it from memory. It also keeps a Postgres database for roster-history diffs and the user-search cache.client/: a Next.js 13 (App Router) app with three tools: the Active Duty Roster (ADR), Roster Statistics, and the Uniform Builder.
The client never talks to the 7th Cavalry API directly. It only talks to the server. The Docker setup below brings the Postgres database up for you, so there's nothing extra to install.
You need two tokens:
| Token | Purpose | Where it goes |
|---|---|---|
API_TOKEN |
Authenticates the server to api.7cav.us. A real 7th Cavalry API bearer. |
server env |
CLIENT_TOKEN |
Shared secret between the client and server. Can be any string you choose, as long as it matches on both sides. | server env + client env |
To get your API_TOKEN:
- Log into your 7th Cavalry Gaming account (member-level, not a public account).
- Open your Connected Accounts and click "view account" for
auth.7cav.us. - Log into Keycloak and copy the provided API token.
Heads up on
.envformatting: useKEY=valuewith no spaces around the=and no surrounding quotes. A line likeAPI_TOKEN ='abc'(note the space) makes the variable nameAPI_TOKEN(with a trailing space), so Docker Compose treatsAPI_TOKENas unset and the server fails to load the roster on startup, then crash-loops instead of coming up.
This is the fastest way to get a working dev environment. It builds and runs both the server and client for you, with hot-reload on the client.
-
Install Docker (Docker Desktop on macOS/Windows).
-
In the project root, copy the example env file and fill in your tokens:
cp .env.example .env
At a minimum set
API_TOKENandCLIENT_TOKEN. Everything else has a sensible local default: the Postgres database is created for you, and the XenForo settings can stay blank (see the notes in.env.example). -
Bring the stack up:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
That's it. The override file (docker-compose.dev.yml) provisions the edge network locally, so you don't need to run docker network create yourself. When it finishes:
- Client (CavApps index): http://localhost:3000
- Server (BFF): http://localhost:4000
The server must successfully load roster data on startup or it will exit and restart. If it keeps restarting, double-check your API_TOKEN (see the formatting note above).
Two features need the forum database. The roster-history diff viewer and the member search box read from the live XenForo (forum) MariaDB, which you won't have locally. The diff viewer just stays empty; the member search returns an error if you use it, since its index is never built. The rest of the app works fine. To enable them, set the
XENFORO_DB_*values in your.envto a reachable XenForo database.
Stop the stack with Ctrl+C, or from another terminal:
docker compose -f docker-compose.yml -f docker-compose.dev.yml down
docker-compose.ymlon its own is the production config and expects an externally managededgenetwork. For local dev always include the-f docker-compose.dev.ymloverride.
If you'd rather run the apps directly on your machine instead of in Docker:
- A valid 7th Cavalry Gaming account with member-level privileges.
- Node.js v18+.
- Your choice of IDE such as VSCode or neoVim.
You'll run the server and client in two separate terminals. The server also needs a Postgres database. If you don't already have one, the easiest path is to run just that container from the compose file:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up postgresThat gives you a database reachable at postgres://cavapps:cavapps@localhost:5432/cavapps. (This URL and the DATABASE_URL below assume the default PG_PASSWORD=cavapps; update both if you set your own.) The Docker Quick Start above avoids all of this; only use manual setup if you specifically need the apps running outside Docker.
1. Server (server/):
cd server
npm install
API_TOKEN=your-7cav-api-token \
CLIENT_TOKEN=any-shared-secret \
DATABASE_URL=postgres://cavapps:cavapps@localhost:5432/cavapps?sslmode=disable \
node server.jsThe server listens on http://localhost:4000. Visiting it in a browser confirms it's up. It reads these values from the environment, so export them in your shell or use a tool like dotenv or a .env loader of your choice. It runs its database migrations on startup and exits if DATABASE_URL isn't reachable.
2. Client (client/):
Create client/.env.local with:
NEXT_PUBLIC_CLIENT_TOKEN=any-shared-secret-you-choose
COMBAT_API_URL=http://localhost:4000/roster/combat
RESERVE_API_URL=http://localhost:4000/roster/reserves
GROUP_API_URL=http://localhost:4000/roster/groups
CACHE_TIMESTAMP_URL=http://localhost:4000/cache-timestamp
NEXT_PUBLIC_INDIVIDUAL_API_URL=http://localhost:4000/roster/individual
NEXT_PUBLIC_DIFF_API_URL=http://localhost:4000
NEXT_PUBLIC_USERCACHE_API_URL=http://localhost:4000/userSearchNEXT_PUBLIC_CLIENT_TOKEN must match the server's CLIENT_TOKEN. Then:
cd client
npm install
npm run devOpen http://localhost:3000 and you should see the CavApps index page. Happy coding!
For further documentation on Next.js, visit https://nextjs.org/docs
Before starting: The ADR and the Roster Statistics page are driven by different files. The ADR reads billet groups by index from
client/app/adr/page.jsx; Roster Statistics reads billet IDs fromBilletBank.jsx. A new billet inside a group the ADR already selects needs no ADR change, because the ADR picks it up live from the API. You only need to updatepage.jsxwhen a new billet group appears or a group moves between categories. Roster Statistics always needs itsBilletBank.jsxlist updated (and, for the regiment chart, a matching color). See Files to Update for the full list.
Since the ADR sources its data from the 7th Cavalry API but selects which billet groups to show from a predefined list, the ADR is not aware when new billet groups are created or when older groups are moved between categories.
For example, if a new company in 2-7 is created, the list the ADR selects from needs to be updated before the new company's membership will display.
The ADR works in terms of billet groups: selecting a group pulls in every billet inside it (e.g. selecting the C/ACD group brings in 1/C/ACD, 3/2/C/ACD, and so on). Each group is selected by its index in the groups array of the API's /roster/groups response.
To add a new billet group to an existing category, you need to update the page.jsx file located in client/app/adr. (A new billet inside a group the ADR already selects needs no change here. See the note above.)
- Open
page.jsx. - Locate the
unitsentry that corresponds to the category where you wish to add the new billet group. - Append the index (referred to as 'ID') of the billet group to that entry's
selectorsarray. (A group's index is its position in the/roster/groupsresponse. Query that endpoint with yourCLIENT_TOKENas theAuthorizationheader to find it.)
Suppose a new billet group is created under Development Command and comes back at index 28. Locate the "Development Command" entry in units and append 28 to its selectors as follows:
{ title: "Development Command", selectors: [21] },{ title: "Development Command", selectors: [21, 28] },To introduce a new category, the units array in client/app/adr/page.jsx needs to be updated.
- In
adr/page.jsx:- Add a new entry to the
unitsarray with atitlefor the new category. - Populate its
selectorsarray with the ID of each billet group that belongs under it. (A group's ID is its position in the/roster/groupsresponse.) - Place the entry where you want it to appear.
unitsrender in array order.
- Add a new entry to the
You have been assigned the task of creating an entry in the ADR for a newly stood-up 5th Battalion. It has 3 companies, Alpha, Bravo and Charlie. Each has its own billet group in the API, returned at indices 29, 30, and 31.
In client/app/adr/page.jsx:
// each number is the index of a billet group in the /roster/groups response
const units = [
...
{ title: "Second Battalion", selectors: [7, 8, 9, 10, 11] },
{ title: "Third Battalion", selectors: [12, 13, 14, 16] },
{ title: "Fifth Battalion", selectors: [29, 30, 31] }, // new
...
];Note: Ensure that you add these elements in the proper locations in
page.jsxto maintain the formatting.
Because the ADR and Roster Statistics are driven separately, adding a billet or unit so it shows up everywhere means touching all of the following. Find the billet group's ID and its billet IDs first by querying /roster/groups (with your CLIENT_TOKEN as the Authorization header).
client/app/adr/page.jsx: the ADR. Add the billet group's ID to aunitsentry'sselectors(existing category) or add a newunitsentry (new category), as shown above.client/app/reusableModules/BilletBank.jsx: the data behind Roster Statistics. Add the billet ID to the matching array (existing category), or add new arrays + a group object for a new category, and export them at the bottom of the file.client/app/rosterstatistics/page.jsx: the Statistics layout. Add the unit to the relevant<Statistics>block'sbilletIDsand add a matching label to itslabelArray(the two must stay the same length and order).client/app/rosterstatistics/modules/statistics.jsx: the chart colors. The regiment-wide chart colors its segments by position from a fixedcolorsarray; if you added a segment to the regimentbilletIDs, add a matching color here. ApexCharts cycles the array when it runs short, so without a new entry the added segment reuses a color already on the chart instead of getting its own.
Note: The billet IDs in
BilletBank.jsxare a hardcoded snapshot and drift as billets are created or moved. The ADR avoids this by reading the API live; Statistics does not, so its lists need occasional refreshing against/roster/groups.
NOTE: If you are making changes to CavApps and want said changes put on the live version, submit a pull request. This section is intended for S6 Staff for deployment testing purposes.
To deploy CavApps on a server, you need the following:
-
A linux (preferably ubuntu) based server with the following:
- Access via SSH
- Sudo level permissions
- Minimum 2GB RAM
-
Alongside the following packages:
- Docker Engine
- nodejs
- npm
- git
sudo apt install git npm nodejs -
A 7th Cavalry API token (see Authorization)
Once the required packages are installed, clone the repo
git clone https://github.com/Vercin-G/CavApps-Test
First, install prerequisites:
In CavApps-Test/server/:
npm install
In CavApps-Test/client/:
npm install
Next, create a .env file in the project root from the template and fill in your tokens (see Authorization):
cp .env.example .envThe docker-compose.yml wires the client to reach the server over the internal Docker network (http://server:4000/...), so you do not need to set the per-URL client variables by hand for a Docker deployment. They're defined in the compose file. It also brings up the Postgres database the server depends on.
Then, from the project root:
docker compose upProduction
docker compose up(without the dev override) expects an externally managededgenetwork. Create it once withdocker network create edgeif it doesn't already exist on the host.
And you should be good! Simply navigate to your server in your browser and the index page should show. The server side should be accessable via port 4000.
NOTE: On slower servers, the generation of nextjs static pages may cause a hang. This is normal. Give it a few seconds.
The Roster Statistics section is currently pending rewrite to include more information. Stay Tuned!
Unlike the ADR, Roster Statistics reads its billet IDs from the BilletBank.jsx file located in client/app/reusableModules.
To add a new billet to an existing category, append the new billet ID to the matching array.
Suppose a new billet with an ID of 531 is added to 1-7's command staff. Update oneSevenCommand as follows:
const oneSevenCommand = ["178", "179", "180", "530"];const oneSevenCommand = ["178", "179", "180", "530", "531"];To introduce a new category, both client/app/reusableModules/BilletBank.jsx and client/app/rosterstatistics/page.jsx need to be updated.
-
In
BilletBank.jsx:- Add a new array for each subcategory and populate it with the required billet IDs.
- Add a new object for the new category and append the subcategories as well as their titles to the new object. Additionally, add a
collapsibleTitlewith the name of the new category into the object. - Add the new object to the
billetBankObjectat the bottom of the file, and add the new arrays to thebilletBankexport list below it so the Statistics page can reach them.
-
In
rosterstatistics/page.jsx:- Add a new
<Statistics>block for the category, listing the new arrays inbilletIDswith a matchinglabelArrayof the same length and order.
- Add a new
-
In
rosterstatistics/modules/statistics.jsx:- If the category also appears in the regiment-wide chart (the one combining every unit), add a color for each new segment to that chart's
colorsarray. It is positional, so add the colors in the same order as the segments. If the array is shorter than the number of segments, ApexCharts loops back to the start and reuses a color, so an uncolored segment comes out the same shade as an existing slice rather than blank. (The per-battalion charts use a separate palette that cycles the same way.)
- If the category also appears in the regiment-wide chart (the one combining every unit), add a color for each new segment to that chart's
Suppose you are adding an entry for a battalion with 3 companies, Alpha, Bravo and Charlie. Each has its own array of billet IDs. The IDs below are placeholders. In a live setting each company's array can run to dozens of entries.
In BilletBank.jsx:
//5-7
const fiveSevenCommand = ["1", "2", "3"]; //placeholder values
const alpha5 = ["4", "5", "6"];
const bravo5 = ["7", "8", "9"];
const charlie5 = ["10", "11", "12"];
const fiveSeven = {
positionIds: [fiveSevenCommand, alpha5, bravo5, charlie5],
positionTitles: [
"5-7 Headquarters",
"Alpha Company",
"Bravo Company",
"Charlie Company",
],
collapsibleTitle: "Fifth Battalion",
};
...
const billetBankObject = {
regi: regi,
oneSeven: oneSeven,
twoSeven: twoSeven,
threeSeven: threeSeven,
fiveSeven: fiveSeven, // new
...
};In rosterstatistics/page.jsx:
<div className="fiveSevenBreakdown">
<div className="Subtitle">Fifth Battalion</div>
<Statistics
billetIDs={[
lists.fiveSevenCommand,
lists.alpha5,
lists.bravo5,
lists.charlie5,
]}
centerLabel="Total 5-7 Strength"
labelArray={[
"5-7 Headquarters",
"Alpha Company",
"Bravo Company",
"Charlie Company",
]}
milpacArray={milpacArray}
/>
</div>In rosterstatistics/modules/statistics.jsx (only if the new battalion is shown in the regiment-wide chart, as line billet battalions are):
// one color per segment, positional: insert them where 5-7's segments sit in
// that chart's billetIDs, not at the end
colors: [
// ...colors for the segments that come before 5-7...
"#5bcefa", // 5-7 HQ
"#5bcefa", // Alpha 5-7
"#5bcefa", // Bravo 5-7
"#5bcefa", // Charlie 5-7
// ...colors for the segments that come after 5-7...
],Note: Ensure that you add these elements in the proper locations to maintain the formatting.