Apps

Frigear website app

The reference host app behind frigear.nu, with its modules, content, environment, database and deployment.

Read this app to see how a complete host app configures the modules, answers their policy hooks and deploys.

The Frigear website app is the code; frigear.nu is the live service it runs, which is also the default issuer. The app is a private Nuxt 4 app on the base layer in apps/website (@frigear-nu/website), deployed to Cloudflare Workers. It replaces the previous Nuxt website.

To run it from the repository root:

pnpm install
cp apps/website/.env.example apps/website/.env   # NUXT_APP_SECRET can stay empty in development
pnpm --filter @frigear-nu/website dev

Modules

nuxt.config.ts extends @frigear-nu/base and installs these modules in this order, after Nuxt modules such as @nuxt/ui, @nuxthub/core, @nuxt/content and @nuxtjs/i18n.

ModuleWhat the Frigear website app configures
UIDefaults.
MenuDefaults. The base layer adds a content main menu.
AuthSign-in at /sign-in, sign-up at /sign-up, password, WebAuthn, magic link, email verification, Facebook and Sign in with frigear.nu.
UsersStaff user management, MCP, email-change verification, profile at /account/profile.
AuthorizationSends visitors on guarded pages to /sign-in.
WidgetDefaults.
AdminOpen to the admin, manager and coordinator roles.
AccountIndex, profile, security, documents and API token pages, plus a list of documents.
NotifyThe log email driver unless NUXT_NOTIFY_* picks a real one.
Email TemplatesBrand name and logo for module emails.
StripeKeys from the environment.
Subscriptionsmonthly, quarterly and yearly membership plans, with price lookup keys from the environment.
PaymentsMCP.
EventsAll pages, including the admin pages, and MCP.
FormsForm and admin pages, MCP and seeded forms (see Forms).
Cookie ConsentPolicy link /legal/gdpr.
OAuth ServerAdmin and applications pages, MCP.
PreferencesDefaults.

In production, sign-up goes to https://frigear.nu/sign-up instead of the local page. The app also depends on Module Kit directly. MCP is not listed: other modules install it as a dependency.

Content

The base layer reads site content from content/. The app has two locales, so it keeps one tree per locale:

FolderContents
content/da, content/enconfig.yaml (name, logo, navigation, footer) and pages/** Markdown. da is the default locale, and en is prefixed.
content/eventsEvent definition YAML files, such as 2026/01.general-assembly.yml.
content/_templates/eventsAn event template for local development.
Nothing reads content/events or content/_templates yet. They hold event definitions ported from the old app. The Events module stores events in the database.

The app's own translations are in i18n/locales. The same files can override module strings, as Module Kit explains.

Environment

Copy apps/website/.env.example to .env. The last column names what reads each variable.

VariablePurposeRead by
NUXT_APP_SECRETSeals the session cookie (Nuxt's appSecret). Empty in development (Nuxt generates one); required in production, at least 32 characters (openssl rand -base64 32).nuxt-auth-utils (Auth)
NUXT_SESSION_PASSWORDFallback encryption key for form submissions and stored OAuth tokens, when NUXT_FORMS_ENCRYPTION_KEY / NUXT_AUTH_TOKENS_KEY are unset.Forms, Auth
NUXT_ACL_ADMINSComma-separated emails that always get the admin role, once the address is verified. Sets up the first admin.The app
NUXT_STRIPE_SECRET_KEY, NUXT_STRIPE_WEBHOOK_SECRET, NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEYStripe credentials.Stripe
SUBSCRIPTIONS_MONTHLY_LOOKUP_KEY, SUBSCRIPTIONS_QUARTERLY_LOOKUP_KEY, SUBSCRIPTIONS_YEARLY_LOOKUP_KEYStripe price lookup keys for the plans. Defaults: membership_month, membership_quarter, membership_annual.Subscriptions
NUXT_OAUTH_SERVER_PRIVATE_KEY, NUXT_OAUTH_SERVER_PREVIOUS_PUBLIC_KEYSThe token signing key, and the retired public keys while rotating.OAuth Server
NUXT_NOTIFY_EMAIL_DRIVER, NUXT_NOTIFY_EMAIL_FROM and the provider keysEmail delivery.Notify
FRIGEAR_SSO_URL, FRIGEAR_SSO_CLIENT_ID, FRIGEAR_SSO_CLIENT_SECRETSign in with frigear.nu. On for pull request previews, or when these are set.Auth
NUXT_FRIGEAR_FORCE_IDUse the frigear.nu user id as the local user id.Auth
NUXT_OAUTH_FACEBOOK_CLIENT_ID, NUXT_OAUTH_FACEBOOK_CLIENT_SECRETFacebook sign-in, offered only when these are set.Auth
NUXT_FORMS_SEED_ENABLED, NUXT_FORMS_APPLICATIONS_TO, NUXT_MAIL_TO, NUXT_CONTACT_<SUBJECT>Forms seeding and recipients (see Forms).The app
DEPLOYMENT_HOST, CLOUDFLARE_WORKER_NAME, NUXT_HUB_CLOUDFLARE_DATABASE_ID, NUXT_HUB_CLOUDFLARE_KV_NAMESPACE_ID, NUXT_HUB_CLOUDFLARE_BUCKET_NAMECloudflare deployment (see Deployment).The app

Forms

The app seeds and guards the Forms module in its own code, not in the module.

  • Seeded forms. forms/index.ts and forms/contact.ts define three forms. contact is published and has a subject select. project-application and board-member-application are drafts. They are inserted only when NUXT_FORMS_SEED_ENABLED=true turns on forms.seed.enabled, and never overwrite an existing slug.
  • Recipients. NUXT_FORMS_APPLICATIONS_TO (comma-separated) receives both application forms. The contact form sends to NUXT_MAIL_TO, and NUXT_CONTACT_<SUBJECT>, such as NUXT_CONTACT_SUPPORT, routes one subject to its own address. Without a recipient, submissions are only stored.
  • Who manages forms. The coordinator role and every role above it hold forms:manage, forms:credentials and forms:pii (authorization.roles in nuxt.config.ts). forms:pii gates submission answers in MCP tools only: the admin pages show submissions with forms:manage.

Access control

nuxt.config.ts defines the roles in authorization.roles. They form a chain, each role inheriting the one below: admin > manager > coordinator > eventmanager > barmanager > crew > member > user. supplier and partner only inherit user. Every signed-in user holds user (defaultRoles).

RoleAdds
crewdocuments:crew: the crew documents on /account/documents (a permission the app registers in authentication.permissions).
eventmanagerevents:manage, events:refund, events:pii.
coordinatoradmin:access (the admin area), users:read, users:list, forms:manage, forms:credentials, forms:pii, payments:manage.
managerusers:create, users:update, users:delete, users:manage, subscriptions:manage, roles:read, roles:assign.
admin*, plus the sensitive events:pii and forms:pii by name, so it holds every permission.
  • The first admin. server/plugins/authorization-roles.ts adds admin to verified emails in NUXT_ACL_ADMINS, in the authorization:roles hook (shared/acl.ts). Nothing is stored. Admins assign every other role on the user's page in the admin. A role can only be assigned by someone whose roles are strictly above it, so a manager assigns coordinator and below, and only admins assign manager or admin.
  • Admin addresses. server/policies.ts lets only admins create a user with, or change an address to, an email in NUXT_ACL_ADMINS, since the account would become an admin once it is verified.
  • Single sign-on claims. server/plugins/oauth-policy.ts adds roles (the effective roles) and role (the most senior one) to ID tokens and userinfo with the profile scope, so an admin here is an admin in the other Frigear apps.

Database

Production uses Cloudflare D1. nuxt.config.ts adds a KV namespace, for Stripe webhook idempotency and the subscription plans cache, and an R2 bucket for form file uploads. Development uses a local SQLite file. Migrations live in apps/website/server/db/migrations/sqlite. Local development has the commands.

Deployment

The app builds with the cloudflare_module Nitro preset and runs on Cloudflare Workers.

  • Production. Pushing to main runs .github/workflows/deploy-production.yml. It deploys and applies the D1 migrations.
  • Previews. Pull requests from collaborators run deploy-preview.yml. Each gets its own D1 database and a Worker Preview (pr-<n>), both deleted when the pull request closes. Both workflows call the reusable deploy-website.yml.
  • Locally. The root cloudflare:* scripts and apps/website/scripts/deploy.sh run the same steps against an existing .output build.

Next steps

Copyright © 2026