Skip to content

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.

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.

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/).

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 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 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.

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 }.

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).

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.

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.

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

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.