Chuck API
Chuck API
Chuck is the Strapi 5 headless CMS that serves marketing
and policy content to brisket. The runtime lives in
apps/chuck/meat/ (see chuck-meat for the
service-local README pointer).
This page documents the HTTP surface, content model, and auth model. For environment variables see chuck-env.
Surface Map
Chuck exposes three HTTP surfaces, all served by the Strapi process bound to
HOST:PORT (config/server.ts, defaults 0.0.0.0:1337, .env.example
sets PORT=1333 for local dev):
| Surface | Path prefix | Consumers |
|---|---|---|
| Content REST API | /api/... | brisket (server-side) |
| Admin panel | /admin | content editors |
| Admin REST API | /admin/api, /users-permissions/* | admin panel only |
GraphQL is not enabled — the @strapi/plugin-graphql package is not
listed in apps/chuck/meat/package.json (verified by grep graphql apps/chuck/meat/package.json apps/chuck/meat/config/plugins.ts, no hits). TODO(@marty): decide whether GraphQL should be enabled; current consumers (brisket) use REST.
Content Types
The content tree lives under apps/chuck/meat/src/api/<api-id>/content-types/<api-id>/schema.json.
At time of writing: 94 content types (92 single-types, 2 collection-types)
plus 21 component categories (~36 components total under
apps/chuck/meat/src/components/).
Collection types
Two collection types — both blog-shaped:
| API ID | Plural | Display |
|---|---|---|
blog | blogs | Academy (apps/chuck/meat/src/api/blog/content-types/blog/schema.json) |
blog-blog | blog-blogs | Blogs (apps/chuck/meat/src/api/blog-blog/content-types/blog-blog/schema.json) |
Both share fields: image (media), title, shortDescription, content
(richtext), author (component author.author), slug (regex
^[a-z0-9]+(?:-[a-z0-9]+)*$).
TODO(@marty): decide which of blog / blog-blog is canonical — the duplicate suggests one is legacy and should be retired. Both directories exist (apps/chuck/meat/src/api/blog/, apps/chuck/meat/src/api/blog-blog/) with identical default routers (createCoreRouter); brisket call sites do not currently reference either by name (no STRAPI_URL consumer hits /api/blogs or /api/blog-blogs in apps/brisket/src).
Single types
Single types are grouped by surface area in brisket. Naming convention is
<page>-<section> or <page>-shared-data.
| Group | Examples | Notes |
|---|---|---|
| Welcome page | welcome-hero-section, welcome-faq-section, welcome-shared-data, welcome-pricing-section, … | Brisket public landing |
| AI homepage | ai-homepage-hero-section, ai-homepage-faq-section, ai-homepage-shared-data, … | Variant landing |
| Homepage (legacy) | homepage-hero-section, homepage-faq-section, homepage-shared-data, … | TODO(@marty): confirm retirement — both homepage-* and ai-homepage-* directories coexist under apps/chuck/meat/src/api/, with no brisket reference to either in apps/brisket/src (only checkout-faq-section is currently wired). |
| For Creators | for-creators-hero-section, for-creators-pricing-section, for-creators-faq-section, for-creators-shared-data, … | Creator funnel |
| Affiliates | affiliates-main-section, affiliates-faq-section, affiliates-become-affiliate-section, … | |
| Showcase | showcase-hero-section, showcase-pricing-section, showcase-shared-data, … | |
| Get Started | get-started-hero-section, get-started-videos-section, … | Onboarding flow |
| Checkout / FAQ | checkout-faq-section, faq-section, cta-section, pricing-section, image-carousel-section | Cross-page widgets |
| Policies | privacy-policy, refund-policy-page, terms-of-service | Legal pages |
| Misc | featured-in, testimonial, testimonial-section, blog-section, academy-section |
Per-page shared-data singles (welcome-shared-data,
ai-homepage-shared-data, etc.) hold cross-section settings — site name,
logos, OG tags, default CTA copy. Example fields in
apps/chuck/meat/src/api/welcome-shared-data/content-types/welcome-shared-data/schema.json:
websiteName, mobileLogo, desktopLogo, copyrightTextAndSlogan,
ctaButtonText, ogTitle, ogDescription, keywords, …
Components
Components live under apps/chuck/meat/src/components/<category>/<name>.json
and are re-used across content types. Categories:
affiliates, author, core-benefits, creator (7 components),
faq, gallery-item, key-point (2), perk, preview-image,
pricing (2), problem, problem-solution, quote, reach-us,
review, rich-content (4), showcase (3), socials, switch,
testimonial (2), video.
REST Endpoints
Strapi auto-generates a CRUD route set per content type via
factories.createCoreRouter (see
apps/chuck/meat/src/api/blog-section/routes/blog-section.ts). For each
single-type and collection-type with the default router:
| Method | Path | Notes |
|---|---|---|
GET | /api/<plural-name> | List (collection) / fetch (single) |
GET | /api/<plural-name>/:id | Fetch one (collection only) |
POST | /api/<plural-name> | Create — admin / authenticated |
PUT | /api/<plural-name>/:id | Update |
DELETE | /api/<plural-name>/:id | Delete |
Strapi 5 query params: ?populate=*, ?populate[field]=...,
?filters[field][$eq]=..., ?fields=..., ?locale=...,
?status=draft|published. All content types declare
"options": { "draftAndPublish": true }.
Concrete consumer evidence
brisket fetches the checkout FAQ section over REST
(apps/brisket/src/server/api/routers/faq.ts:22):
const url = `${env.STRAPI_URL}/api/checkout-faq-section?populate=*`;const res = await fetch(url, { headers: { Authorization: `Bearer ${env.STRAPI_TOKEN}` },});STRAPI_URL and STRAPI_TOKEN are required env in
apps/brisket/src/env.js:31-32. Any other brisket → chuck call sites
should follow the same pattern. At time of writing, the only brisket
fetcher of STRAPI_URL is apps/brisket/src/server/api/routers/faq.ts
(verified via grep -rn 'STRAPI_URL' apps/brisket/src).
Auth Model
Chuck has three independent token domains, all configured in
apps/chuck/meat/config/admin.ts:
| Token kind | Salt / secret env | Used by | Notes |
|---|---|---|---|
| Admin JWT | ADMIN_JWT_SECRET | Admin panel session cookies | Issued on admin login |
| API tokens | API_TOKEN_SALT | Service callers (brisket) | Created per-token in /admin → Settings → API Tokens. Sent as Authorization: Bearer <token>. |
| Transfer tokens | TRANSFER_TOKEN_SALT | strapi transfer data export/import | Used by ops, not runtime consumers |
| App keys | APP_KEYS (array) | Koa session signing | Rotated by adding new keys to the front of the array |
The users-permissions plugin (@strapi/plugin-users-permissions@5.5.1,
apps/chuck/meat/package.json) is installed but not currently used as
a consumer auth path — brisket uses an API token. End-user accounts on
chuck would also need JWT_SECRET (see .env.example); TODO(@marty):
confirm whether any product flow needs end-user auth on chuck — no brisket
call site references the chuck users-permissions endpoints today.
Anchor: the multi-service auth picture lives in standards/auth-model; the trust-boundary picture lives in standards/security-model. Chuck → brisket is a static-bearer-token boundary.
Permissions
The “Public” role permissions (which find / findOne are anonymous on
each content type) are stored in the database (Strapi
users-permissions), not in code. There is no policy file checked into
apps/chuck/meat/src/api/*/routes/ — every router file uses
factories.createCoreRouter(...) with no policies override. brisket’s
only known fetch path (apps/brisket/src/server/api/routers/faq.ts)
sends Authorization: Bearer ${env.STRAPI_TOKEN}, so the API-token path
is exercised; TODO(@marty): confirm whether any anonymous (Public-role)
read paths exist in production and document the role-bootstrap step in
chuck-runbook if so.
Plugins
From apps/chuck/meat/package.json and config/plugins.ts:
| Plugin | Version | Purpose |
|---|---|---|
@strapi/plugin-cloud | 5.5.1 | Strapi Cloud integration (idle if not used) |
@strapi/plugin-users-permissions | 5.5.1 | End-user auth + role permissions table |
@strapi/provider-email-nodemailer | ^5.9.0 | Email provider — wired in config/plugins.ts email block |
strapi-provider-cloudflare-r2 | ^0.3.0 | Upload provider — wired in config/plugins.ts upload block |
strapi-plugin-tablify | ^1.0.3 | Admin UI: table view in richtext editor |
Security Middleware
apps/chuck/meat/config/middlewares.ts adds connect-src, img-src,
media-src allowlists pointing at market-assets.strapi.io and the R2
public URL (CF_PUBLIC_ACCESS_URL). strapi::cors is enabled with its
default config (no overrides in apps/chuck/meat/config/middlewares.ts,
which means the Strapi default origin: '*' applies) — TODO(@marty):
decide whether the origin allowlist needs tightening for production.
Anchor Docs
- Service overview: chuck and chuck-meat
- Env vars: chuck-env
- Errors: chuck-errors
- Runbook: chuck-runbook
- Local dev: chuck-local-dev
- Auth model: standards/auth-model
- Security model: standards/security-model