Frigear website app
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.
| Module | What the Frigear website app configures |
|---|---|
| UI | Defaults. |
| Menu | Defaults. The base layer adds a content main menu. |
| Auth | Sign-in at /sign-in, sign-up at /sign-up, password, WebAuthn, magic link, email verification, Facebook and Sign in with frigear.nu. |
| Users | Staff user management, MCP, email-change verification, profile at /account/profile. |
| Authorization | Sends visitors on guarded pages to /sign-in. |
| Widget | Defaults. |
| Admin | Open to the admin, manager and coordinator roles. |
| Account | Index, profile, security, documents and API token pages, plus a list of documents. |
| Notify | The log email driver unless NUXT_NOTIFY_* picks a real one. |
| Email Templates | Brand name and logo for module emails. |
| Stripe | Keys from the environment. |
| Subscriptions | monthly, quarterly and yearly membership plans, with price lookup keys from the environment. |
| Payments | MCP. |
| Events | All pages, including the admin pages, and MCP. |
| Forms | Form and admin pages, MCP and seeded forms (see Forms). |
| Cookie Consent | Policy link /legal/gdpr. |
| OAuth Server | Admin and applications pages, MCP. |
| Preferences | Defaults. |
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:
| Folder | Contents |
|---|---|
content/da, content/en | config.yaml (name, logo, navigation, footer) and pages/** Markdown. da is the default locale, and en is prefixed. |
content/events | Event definition YAML files, such as 2026/01.general-assembly.yml. |
content/_templates/events | An event template for local development. |
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.
| Variable | Purpose | Read by |
|---|---|---|
NUXT_APP_SECRET | Seals 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_PASSWORD | Fallback encryption key for form submissions and stored OAuth tokens, when NUXT_FORMS_ENCRYPTION_KEY / NUXT_AUTH_TOKENS_KEY are unset. | Forms, Auth |
NUXT_ACL_ADMINS | Comma-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_KEY | Stripe credentials. | Stripe |
SUBSCRIPTIONS_MONTHLY_LOOKUP_KEY, SUBSCRIPTIONS_QUARTERLY_LOOKUP_KEY, SUBSCRIPTIONS_YEARLY_LOOKUP_KEY | Stripe price lookup keys for the plans. Defaults: membership_month, membership_quarter, membership_annual. | Subscriptions |
NUXT_OAUTH_SERVER_PRIVATE_KEY, NUXT_OAUTH_SERVER_PREVIOUS_PUBLIC_KEYS | The token signing key, and the retired public keys while rotating. | OAuth Server |
NUXT_NOTIFY_EMAIL_DRIVER, NUXT_NOTIFY_EMAIL_FROM and the provider keys | Email delivery. | Notify |
FRIGEAR_SSO_URL, FRIGEAR_SSO_CLIENT_ID, FRIGEAR_SSO_CLIENT_SECRET | Sign in with frigear.nu. On for pull request previews, or when these are set. | Auth |
NUXT_FRIGEAR_FORCE_ID | Use the frigear.nu user id as the local user id. | Auth |
NUXT_OAUTH_FACEBOOK_CLIENT_ID, NUXT_OAUTH_FACEBOOK_CLIENT_SECRET | Facebook 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_NAME | Cloudflare 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.tsandforms/contact.tsdefine three forms.contactis published and has a subject select.project-applicationandboard-member-applicationare drafts. They are inserted only whenNUXT_FORMS_SEED_ENABLED=trueturns onforms.seed.enabled, and never overwrite an existing slug. - Recipients.
NUXT_FORMS_APPLICATIONS_TO(comma-separated) receives both application forms. The contact form sends toNUXT_MAIL_TO, andNUXT_CONTACT_<SUBJECT>, such asNUXT_CONTACT_SUPPORT, routes one subject to its own address. Without a recipient, submissions are only stored. - Who manages forms. The
coordinatorrole and every role above it holdforms:manage,forms:credentialsandforms:pii(authorization.rolesinnuxt.config.ts).forms:piigates submission answers in MCP tools only: the admin pages show submissions withforms: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).
| Role | Adds |
|---|---|
crew | documents:crew: the crew documents on /account/documents (a permission the app registers in authentication.permissions). |
eventmanager | events:manage, events:refund, events:pii. |
coordinator | admin:access (the admin area), users:read, users:list, forms:manage, forms:credentials, forms:pii, payments:manage. |
manager | users: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.tsaddsadminto verified emails inNUXT_ACL_ADMINS, in theauthorization:roleshook (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.tslets only admins create a user with, or change an address to, an email inNUXT_ACL_ADMINS, since the account would become an admin once it is verified. - Single sign-on claims.
server/plugins/oauth-policy.tsaddsroles(the effective roles) androle(the most senior one) to ID tokens and userinfo with theprofilescope, 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
mainruns.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 reusabledeploy-website.yml. - Locally. The root
cloudflare:*scripts andapps/website/scripts/deploy.shrun the same steps against an existing.outputbuild.
Next steps
- Local development – run the app and its database on your machine.
- Deployment – deploy your own app the same way.
Standalone mobile starter
Start a mobile app with its own backend - a Nuxt 4 web app that is the OAuth server and API, and a Capacitor app that signs in at it - from the standalone mobile starter.
Protect a route
Declare a permission with defineAccess, check it on a server route with authorize, keep money and credential actions to cookie sessions, and guard pages with route rules.