Skip to content

dotcomrow/internal

Repository files navigation

Suncoast Home App Shell

Mobile-first Next.js app shell deployed to Cloudflare Workers using OpenNext.

Runtime Architecture

  1. User requests read app-shell content from Cloudflare D1 (CACHE binding).
  2. Runtime GraphQL calls are made through the GRAPHQL service binding.
  3. Runtime GraphQL calls use OAuth client-credentials to obtain a bearer token.
  4. DIRECTUS_CACHE_ALLOW_RUNTIME_REFRESH controls whether user traffic can refresh stale cache entries from origin.
  5. DIRECTUS_CACHE_SEED_ON_MISS controls one-time bootstrap seeding when cache is empty (useful on first deploy).
  6. Cache refresh can also run out-of-band through POST /api/cache/refresh.
  7. HTML and static asset cache headers are set in src/middleware.ts.
  8. Browser clients call GET /api/cache/version and reload only when contentHash changes.
  9. Directus media is served from /assets/<file-id> by the shell Worker, backed by R2 (MEDIA) and fetched through a Hasura GraphQL action over the GraphQL proxy service binding.

Homepage origin query uses GraphQL read-items mutations generated from:

  • DIRECTUS_CONTENT_COLLECTION (default cms_pages)
  • DIRECTUS_LAYOUT_COLLECTION (default cms_layout_templates)

The shell fetches the page row, resolves layout_template_key, fetches the template HTML/CSS, renders slot placeholders ({{ slot:main }}, etc.), and serves CMS HTML directly.

Browser Cache Versioning

  • HTML responses default to Cache-Control: public, max-age=0, must-revalidate (override with app_html_cache_control).
  • Next build assets under /_next/static/* use Cache-Control: public, max-age=31536000, immutable via public/_headers.
  • Worker-handled static routes default to Cache-Control: public, max-age=31536000, immutable (override with app_static_cache_control).
  • API routes default to Cache-Control: no-store, no-cache, must-revalidate.
  • The page includes a content hash (data-content-hash) and checks /api/cache/version.
  • If the hash changes after a Directus refresh/deploy, the client reloads with ?cmsv=<hash>.
  • If the hash is unchanged, the browser keeps using cached content.

Deployment Model

Deployments are Terraform-first:

  1. GitHub Action builds OpenNext output (.open-next/*).
    • Build runs a post-step patch (scripts/patch-prefetch-hints-loader.mjs) so generated Workers accept new Next.js 16.2 manifest lookups (for example prefetch-hints and subresource-integrity).
  2. GitHub Action runs wrangler deploy --dry-run to produce a bundled worker.js.
  3. Worker bundle + assets are copied into terraform/.open-next.
  4. Terraform Cloud applies (workspace execution mode is enforced to agent):
    • cloudflare_worker (ensures the Worker service exists)
    • cloudflare_worker_version (Worker code + bindings + assets)
    • cloudflare_workers_deployment (ships latest version to 100%)
    • D1 database resources (cloudflare_d1_database)
    • optional cloudflare_workers_custom_domain mappings

Wrangler commands are still available for local preview/testing.

Branch mapping in .github/workflows/terraform-deploy.yml:

  • prod -> workspace <repo-name> -> deployment_environment=production
  • dev -> workspace <repo-name>-preview -> deployment_environment=preview
  • workflow also sets manage_d1_resources=true on prod and false on dev
  • workflow sets manage_r2_resources=true on prod (R2 owner) and false on dev to keep first-run bucket creation and ongoing ownership consistent
  • optional GitHub Repository Variables can override workspace names:
    • TFC_WORKSPACE_PRODUCTION
    • TFC_WORKSPACE_PREVIEW
  • if the target workspace does not exist, deploy workflow fails (workspace creation belongs to initial-deploy.yml)

Required GitHub Variables

Set this at repository or organization scope:

  • TFE_AGENT_POOL_ID (required; Terraform Deploy and Initial Deploy enforce workspace execution-mode=agent and this pool id)
  • KEYCLOAK_REALM (required; must be internal or external; workflows fail fast if missing/invalid and propagate this into Terraform keycloak_realm)
  • KEYCLOAK_AUTH_HOST (optional; defaults to auth-origin.suncoast.systems; used by deploy workflows when seeding directus_auth_token_url, cache_refresh_auth_issuer, and cache_refresh_auth_jwks_url)
  • APP_AUTH_GATEWAY_URL (optional; defaults to https://login.suncoast.systems; browser login gateway URL injected into the Worker)
  • APP_AUTH_GATEWAY_ADMIN_URL (optional; defaults to https://login.suncoast.systems; passed to Terraform so the Terraform agent can register/update /v1/apps)
  • APP_BASE_DOMAIN (optional; defaults to suncoast.systems; used to derive auth-gateway app base URLs)
  • APP_AUTH_BASE_URL_PRODUCTION (optional; explicit production app URL for auth-gateway allowlist; default https://<repo-name>.<APP_BASE_DOMAIN>)
  • APP_AUTH_BASE_URL_PREVIEW (optional; explicit preview app URL for auth-gateway allowlist; default https://<repo-name>-preview.<APP_BASE_DOMAIN>)

Required GitHub secret:

  • AUTH_GATEWAY_ADMIN_TOKEN (required; workflows pass this to Terraform as TF_VAR_app_auth_gateway_admin_token for authenticated /v1/apps writes)

If workspace execution mode still shows remote, check HCP Terraform project defaults/policies: workspace-level overrides for execution mode and agent pool must be allowed (or project defaults must already point to the same agent pool).

Terraform Variables For Worker Domains

Terraform now maps custom domains to deployed Worker services using cloudflare_workers_custom_domain. Terraform also maps URL routes to Worker services using cloudflare_workers_route.

  • worker_service_name_production (optional; default <project_name>)
  • worker_service_name_preview (optional; default <project_name>-preview)
  • worker_preview_hostname (optional; default <project_name>-preview.<domain>)
  • manage_worker_domains (default true; set false to skip custom domain attachment)
  • manage_worker_routes (default true; set false to skip route management)
  • worker_production_route_pattern (optional; default <project_name>.<domain>/*)
  • worker_preview_route_pattern (optional; default <worker_preview_hostname>/*)
  • legacy worker_domain_environment_production / worker_domain_environment_preview are still used as route pattern fallbacks when the new route variables are empty
  • enable_workers_dev_subdomain / enable_workers_dev_previews (default true; controls workers.dev visibility and preview links)
  • enable_worker_observability (default true)
  • enable_worker_observability_logs (default true)
  • enable_worker_observability_invocation_logs (default true)
  • worker_observability_head_sampling_rate / worker_observability_logs_head_sampling_rate (default 1.0)

Required Terraform Variables

Set these in Terraform Cloud workspace variables (sensitive where applicable):

  • cloudflare_token
  • cloudflare_account_id
  • cloudflare_zone_id
  • keycloak_realm (required; canonical realm selector for auth/content/bearer defaults; must be internal or external)
  • directus_client_lookup_from_vault (default true; when enabled, Vault values are always used for worker Directus auth)
  • directus_client_id (only used when directus_client_lookup_from_vault=false)
  • directus_client_secret (only used when directus_client_lookup_from_vault=false)
  • directus_client_id_vault_name_template (default keycloak-client-id-cms-site-{site_key})
  • directus_client_secret_vault_name_template (default keycloak-client-secret-cms-site-{site_key})
  • directus_auth_token_url (optional override; leave empty to use https://<keycloak_auth_dns_name>.<domain><keycloak_auth_token_path>)
  • keycloak_auth_token_path (optional override; defaults to /realms/<keycloak_realm>/protocol/openid-connect/token)
  • keycloak_auth_jwks_path (optional override; defaults to /realms/<keycloak_realm>/protocol/openid-connect/certs)
  • directus_content_site_key (optional override; defaults to <keycloak_realm>)
  • cache_refresh_auth_allowed_clients (optional override; defaults to cms-site-<keycloak_realm>)
  • manage_keycloak_auth_dns_record (set true in the shared-owner workspace to manage auth DNS through tunnel)
  • manage_keycloak_tunnel_config (set true in the shared-owner workspace to manage tunnel ingress)
  • keycloak_tunnel_shared_owner_environment (default preview; only this environment manages shared auth DNS+tunnel config)
  • keycloak_auth_dns_name (default auth)
  • keycloak_tunnel_id (required when manage_keycloak_auth_dns_record=true)
  • keycloak_tunnel_service (required when manage_keycloak_tunnel_config=true, must be http:// or https://)
  • directus_auth_scope (optional)
  • directus_token_request_max_attempts (default 2)
  • directus_token_request_retry_base_ms (default 250)
  • directus_token_request_timeout_ms (default 6000)
  • cache_refresh_auth_audience_lookup_from_vault (default true; when enabled, Vault value is used for refresh + Directus token audience)
  • directus_auth_audience (only used when cache_refresh_auth_audience_lookup_from_vault=false)
  • cache_refresh_auth_audience_vault_name (default keycloak-client-id-graphql-api)
  • vault_addr / vault_token / vault_namespace (required when Vault lookups are enabled)
  • directus_allow_client_header_auth_fallback (default false; set true only if you want non-bearer fallback)
  • app_auth_enabled (optional; defaults to true when gateway or OIDC browser auth config is present)
  • app_auth_gateway_url (optional; default https://login.suncoast.systems)
  • app_auth_gateway_admin_url (optional; default https://login.suncoast.systems; used by Terraform agent for auth-gateway app registration)
  • app_auth_gateway_admin_token (sensitive; required when manage_auth_gateway_app_registrations=true; bearer token used by Terraform agent for auth-gateway app registration)
  • manage_auth_gateway_app_registrations (default true; when enabled, Terraform agent keeps auth-gateway app slugs/base URLs up to date via /v1/apps)
  • app_auth_gateway_request_timeout_seconds (default 30; timeout for Terraform agent auth-gateway API requests)
  • app_auth_base_url_production (optional; default https://<project_name>.<domain> for auth-gateway registration)
  • app_auth_base_url_preview (optional; default https://<project_name>-preview.<domain> for auth-gateway registration)
  • app_auth_app_slug_production (optional; default slugified project_name; must exist in auth-gateway allowed apps)
  • app_auth_app_slug_preview (optional; default <slugified project_name>-preview; must exist in auth-gateway allowed apps)
  • app_auth_code_param (optional; default gateway_code)
  • app_auth_start_path (optional; default /start)
  • app_auth_exchange_path (optional; default /v1/auth/exchange)
  • app_auth_client_id (optional override; in gateway mode defaults to auth-gateway-public, otherwise falls back to DIRECTUS_CLIENT_ID)
  • app_auth_authorization_url (optional; defaults from token URL by replacing /token with /auth)
  • app_auth_token_url (optional; defaults to DIRECTUS_AUTH_TOKEN_URL)
  • app_auth_userinfo_url (optional; defaults from token URL by replacing /token with /userinfo)
  • app_auth_logout_url (optional; defaults from token URL by replacing /token with /logout)
  • app_auth_scope (optional; default openid profile email)
  • app_auth_audience (optional)
  • app_auth_account_referrer (optional fallback/override for Keycloak account referrer; default auth-gateway-public)
  • app_auth_account_referrer_uri_external (optional fallback/override for Keycloak account referrer_uri; popup defaults to current full page URL)
  • app_auth_account_referrer_uri_internal (optional fallback/override for Keycloak account referrer_uri; popup defaults to current full page URL)
  • app_auth_redirect_uri (optional; defaults to <origin>/auth/callback)
  • app_auth_post_logout_redirect_uri (optional; defaults to <origin>)
  • directus_layout_collection (default cms_layout_templates)
  • directus_layout_fields (default id,template_key,status,site_key,html,css)
  • directus_page_layout_field (default layout_template_key)
  • directus_layout_template_key_field (default template_key)
  • directus_layout_html_field (default html)
  • directus_layout_css_field (default css)
  • directus_page_theme_field (default theme_package_key)
  • directus_page_theme_mode_field (default theme_mode)
  • directus_page_theme_switcher_position_field (default theme_switcher_position)
  • directus_page_theme_switcher_dock_direction_field (default theme_switcher_dock_direction)
  • directus_page_ga_measurement_id_field (default analytics_google_measurement_id)
  • directus_page_openobserve_rum_script_url_field (default analytics_openobserve_rum_script_url)
  • directus_page_openobserve_rum_config_field (default analytics_openobserve_rum_config)
  • app_monitoring_script_src (optional URL)
  • app_monitoring_script_inline (optional inline JS)
  • app_google_ads_client_id (optional ads client id)
  • app_html_cache_control (optional override for HTML cache header)
  • app_static_cache_control (optional override for static asset cache header)
  • app_content_version_check_enabled (optional, default true)
  • app_content_version_endpoint (optional, default /api/cache/version)
  • app_content_version_query_param (optional, default cmsv)
  • directus_cache_allow_runtime_refresh (optional, default false)
  • directus_cache_seed_on_miss (optional, default true; allows first-request cache bootstrap)
  • directus_media_proxy_endpoint (optional, default https://graphql.internal)
  • directus_media_fetch_image_action (optional, default empty; supports one or more comma-separated mutation names, and the Worker auto-discovers/falls back when not set)
  • directus_media_fetch_timeout_seconds (optional, default 20)
  • directus_media_fetch_max_bytes (optional, default 52428800)
  • directus_media_proxy_asset_path_prefix (legacy passthrough setting; kept for compatibility)
  • directus_media_cache_control (optional, default public, max-age=31536000, immutable)
  • cache_refresh_auth_issuer (optional override; defaults to https://<keycloak_auth_hostname>/realms/<keycloak_realm>)
  • cache_refresh_auth_jwks_url (sensitive recommended)
  • GCP_LOGGING_CREDENTIALS (sensitive)
  • plus existing non-secret app vars (domain, project_name, etc.)

By default, the template derives D1/R2 names from project_name:

  • d1_dev_cache_name -> <project_name>-dev-cache
  • d1_prod_cache_name -> <project_name>-prod-cache
  • r2_dev_media_cache_name -> <project_name>-dev-media-cache
  • r2_prod_media_cache_name -> <project_name>-prod-media-cache

During initial-deploy.yml, these are explicitly written to Terraform workspace variables from a sanitized repository-name slug.

You can override any of the four names with explicit Terraform workspace variables. Set d1_dev_read_replication_mode / d1_prod_read_replication_mode to match your current D1 configuration (disabled or auto). Keep manage_r2_resources=true for the workspace that owns bucket creation.

If keycloak_auth_dns_name already exists in Cloudflare and is not in Terraform state yet, import that DNS record before enabling manage_keycloak_auth_dns_record=true. When manage_keycloak_tunnel_config=true, Terraform manages tunnel ingress config for that tunnel id. Use a dedicated tunnel (or one shared config owner) to avoid overwriting rules managed elsewhere.

For preview branch deploys, attach the same required secrets/variables (or variable sets) to the preview workspace as production.

Local Development

Run standard Next.js dev:

npm install
npm run dev

Run worker preview of the production build:

npm run cf:preview

Build And Deploy

Apply Terraform (D1 + Worker deploy + optional custom domains):

cd terraform
terraform apply

In GitHub Actions (.github/workflows/terraform-deploy.yml):

  • pushes to dev deploy preview Worker via Terraform workspace <repo-name>-preview
  • pushes to prod deploy production Worker via Terraform workspace <repo-name>

Cache Refresh Endpoint

curl -X POST "https://<app-domain>/api/cache/refresh" \
  -H "Authorization: Bearer <access_token>"

Modes:

  • Default mode is controlled by DIRECTUS_CACHE_REFRESH_MODE_DEFAULT (Terraform variable directus_cache_refresh_mode_default), default purge-and-refresh.
  • Override per call with query or JSON body:
    • POST /api/cache/refresh?mode=refresh
    • POST /api/cache/refresh?mode=purge
    • POST /api/cache/refresh?mode=purge-and-refresh
  • JSON body also supports purge_all: true|false when mode is omitted.
  • Cache rows are site-scoped by default (app-shell:<collection>:<site_key>:<slug-or-all>), so internal/external home pages do not collide.
  • Optional DIRECTUS_CACHE_KEY supports tokens: {collection}, {site_key} (or {site}), {slug}, {scope}.

Scheduled Refresh Worker (Optional)

Use:

  • workers/cache-refresher/worker.ts
  • workers/cache-refresher/wrangler.toml.example

Typical Keycloak config:

  • TOKEN_URL = "https://auth-origin.suncoast.systems/realms/<keycloak_realm>/protocol/openid-connect/token"
  • TOKEN_AUDIENCE = "<graphql-api-client-id>"

Deploy refresher worker:

wrangler deploy --config workers/cache-refresher/wrangler.toml

Terraform Note

Terraform now provisions:

  • D1 cache databases (CACHE preview + production)
  • Production Worker service + version + deployment (cloudflare_worker + cloudflare_worker_version + cloudflare_workers_deployment)
  • Runtime bindings and secrets for the app Worker
  • Optional Worker custom domain mappings (cloudflare_workers_custom_domain when manage_worker_domains=true)
  • Worker route mappings (cloudflare_workers_route when manage_worker_routes=true)

Auth/JWKS defaults:

  • KEYCLOAK_REALM is the canonical selector for internal vs external behavior across Terraform and worker runtime defaults.
  • DIRECTUS_AUTH_TOKEN_URL resolves to the managed Keycloak tunnel URL when directus_auth_token_url is empty.
  • CACHE_REFRESH_AUTH_ISSUER defaults to https://<keycloak_auth_hostname>/realms/<keycloak_realm> when cache_refresh_auth_issuer is empty.
  • CACHE_REFRESH_AUTH_JWKS_URL resolves from cache_refresh_auth_issuer first (<issuer>/protocol/openid-connect/certs), then falls back to token URL derivation (.../token -> .../certs) when cache_refresh_auth_jwks_url is empty.
  • CACHE_REFRESH_AUTH_JWKS_TIMEOUT_MS is set from Terraform variable cache_refresh_auth_jwks_timeout_ms (default 12000).
  • CACHE_REFRESH_AUTH_JWKS_FETCH_MAX_ATTEMPTS and CACHE_REFRESH_AUTH_JWKS_FETCH_RETRY_BASE_MS control per-URL JWKS retry behavior.
  • CACHE_REFRESH_AUTH_JWKS_STALE_MAX_AGE_MS allows stale JWKS fallback when upstream cert endpoints are temporarily unavailable.
  • Runtime auth verification also tries issuer-derived JWKS from the incoming token (iss) to handle auth host aliasing (auth-origin vs auth).
  • Refresh endpoint retry controls are exposed via cache_refresh_max_attempts and cache_refresh_retry_base_ms.

Browser Login Runtime

The shell now exposes a browser auth runtime for both in-shell components and MFEs:

  • Floating profile button is rendered beside the widget dock.
  • Guest state uses a default profile silhouette icon.
  • Signed-in state uses profile picture/avatar (with deterministic fallback avatar URL when picture is missing).
  • Browser login uses auth-gateway by default (https://login.suncoast.systems/start + /v1/auth/exchange).
  • Login callback route remains /auth/callback and exchanges gateway_code (or configured code param).
  • OIDC direct auth (authorization_url + token_url) remains available as fallback.
  • Build stamp (Build <version+sha@timestamp>) is shown in the auth profile popup; if commit/timestamp are unavailable, it falls back to package version and Next build ID.
  • Shell runtime schedules automatic session refresh about 1 minute before token expiry (max cadence 14 minutes).
  • Shell (not MFE) shows a session-expiry modal starting ~2m30s before token expiry with refresh and log-out actions.
  • Shell runtime enforces token-expiry logout at exp time (same auth logout flow as profile-menu sign-out), even if the modal is not visible.
  • Browser auth writes a shared hint cookie (suncoast_auth_present) on *.suncoast.systems so sibling apps can auto-bootstrap sign-in and local sign-out sync when users move between apps.

Global API for MFEs:

  • window.__SUNCOAST_AUTH__.isLoggedIn()
  • window.__SUNCOAST_AUTH__.getState()
  • window.__SUNCOAST_AUTH__.getAccessToken()
  • window.__SUNCOAST_AUTH__.getRefreshToken()
  • window.__SUNCOAST_AUTH__.getProfile()
  • window.__SUNCOAST_AUTH__.login() / logout()
  • window.__SUNCOAST_AUTH__.refreshSession(force?)
  • window.__SUNCOAST_AUTH__.ensureFreshToken(minValiditySeconds?)
  • window.__SUNCOAST_AUTH__.updateToken(minValiditySeconds?)
  • window.__SUNCOAST_AUTH__.refreshAccessToken(minValiditySeconds?)
  • window.__SUNCOAST_AUTH__.refreshToken(minValiditySeconds?)
  • window.__SUNCOAST_AUTH__.subscribe(listener)

Runtime snapshot also lives at window.__SUNCOAST_RUNTIME__.auth.

iOS And Android Wrapping (Capacitor)

  1. Update capacitor.config.json server.url to your deployed domain.
  2. Add native platforms:
npm run mobile:add:ios
npm run mobile:add:android
  1. Sync and open native projects:
npm run mobile:sync
npm run mobile:open:ios
npm run mobile:open:android

About

internal app shell

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors