Vuesion builds on the configuration mechanisms provided by Nuxt, Prisma, and the surrounding tooling.
Rather than introducing another custom configuration layer, Vuesion follows the conventions of the technologies it is built on.
This makes the project immediately familiar to developers already working with the Nuxt ecosystem while keeping configuration predictable.
Configuration is typically divided into three categories:
Environment
│
▼
Framework
│
▼
Project
Each layer has a different responsibility.
Environment variables configure values that differ between environments or contain sensitive information.
Typical examples include:
Vuesion intentionally keeps these values outside the source code.
For example:
DATABASE_URL=...
NUXT_SESSION_PASSWORD=...
RESEND_API_KEY=...
Some environment variables are required for every installation.
Examples include:
DATABASE_URLNUXT_SESSION_PASSWORDOthers only become necessary when a particular feature is enabled.
For example:
NUXT_OAUTH_GITHUB_CLIENT_IDNUXT_OAUTH_GITHUB_CLIENT_SECRETRESEND_API_KEYNUXT_SUPABASE_URLNUXT_SUPABASE_SERVICE_ROLE_KEYNUXT_SUPABASE_STORAGE_BUCKETNUXT_PUBLIC_CDN_URLNUXT_LEMON_SQUEEZY_API_KEY, the API key of the Lemon Squeezy environment (Test Mode or Live Mode)NUXT_LEMON_SQUEEZY_STORE_ID, the ID of the store (the id returned by GET /v1/stores)NUXT_LEMON_SQUEEZY_WEBHOOK_SECRET, the signing secret entered for the Lemon Squeezy webhookNUXT_PUBLIC_BASE_URL, the externally reachable URL of the installation, e.g. https://app.example.com or your tunnel URL. The checkout returns the customer to <NUXT_PUBLIC_BASE_URL>/workspaces/:id/settings?checkout=returned, so a wrong value sends them to another installation or an unreachable address.NUXT_BILLING_REFERENCE_SECRET, a secret that does not come from Lemon Squeezy: the application uses it to sign the workspace reference of a checkout. Vuesion only requires it to be non-empty; generate a long random value, e.g. with openssl rand -base64 32, and avoid changing it while checkouts are open: a subscription from a checkout started before the change cannot be linked to its workspace.This keeps local development lightweight while allowing production installations to enable additional services as needed.
The application starts without billing configuration. Billing operations fail with 500 Billing is not configured: ... until the variables are set.
Lemon Squeezy separates Test Mode and Live Mode completely: API keys created in Test Mode only work with test data, and products copied to Live Mode get new IDs. Vuesion therefore has no test mode switch. The credentials and data of each deployment decide the mode:
public demo (NUXT_PUBLIC_DEMO_MODE=true)
└── simulated billing, no Lemon Squeezy configuration
development / staging
├── Test Mode API key and store ID
└── PlanPrice External IDs of the Test Mode variants
production
├── Live Mode API key and store ID
└── PlanPrice External IDs of the Live Mode variants
Each deployment configures one set of credentials. Use Test Mode to verify the real integration, including checkout, webhooks, and the Customer Portal, before going live.
The public demo does not use Test Mode, because a Test Mode checkout is still a real Lemon Squeezy checkout: anonymous visitors would see a payment form, enter (test) card data, and create customers and subscriptions in your store, and the demo would depend on webhooks reaching a disposable deployment. Instead, demo mode replaces the provider with a local simulation (see Simulated billing under Demo mode). A demo deployment needs none of the NUXT_LEMON_SQUEEZY_* variables and no NUXT_BILLING_REFERENCE_SECRET.
A plan price points to a Lemon Squeezy variant:
Lemon Squeezy product
↓
Lemon Squeezy variant
↓ variant ID
PlanPrice.externalId
Monthly and yearly billing normally use separate variants and therefore separate plan prices with different External IDs. Vuesion stores no product IDs. The amount and currency of a plan price are what Vuesion displays; Lemon Squeezy charges the price of the variant, so keep both in line.
Configure the customer portal of the store under Design → Portal → Subscriptions:
Update subscription plans ON
Pause subscriptions OFF
Update subscription quantity OFF
When a plan change needs the customer's confirmation, e.g. for PayPal subscriptions, Vuesion opens Lemon Squeezy's hosted subscription update page. Lemon Squeezy rejects that page ("Updating subscriptions is not allowed") unless Update subscription plans is enabled. Vuesion does not manage paused subscriptions or quantities, so these options stay off.
Create a webhook in the Lemon Squeezy store (Settings → Webhooks, separately in Test Mode and Live Mode):
URL: https://<host>/api/webhooks/lemonsqueezy
Secret: the value of NUXT_LEMON_SQUEEZY_WEBHOOK_SECRET
Events: subscription_created, subscription_updated
subscription_updated fires for every later change, including cancellation, failed payments, and expiration, so no other events are needed. Additional events are acknowledged and ignored.
The webhook payload is only a trigger: Vuesion fetches the current subscription from the Lemon Squeezy API and synchronizes that state, so duplicate, delayed, or out-of-order webhooks are harmless. Vuesion does not store webhook events. When the synchronization fails, the endpoint returns an error and Lemon Squeezy retries the delivery up to three times; failed deliveries can be resent from the Lemon Squeezy dashboard. The endpoint is excluded from the request rate limiter in nuxt.config.ts.
To test the complete flow in Test Mode:
NUXT_BILLING_REFERENCE_SECRET.4242 4242 4242 4242.The PayPal confirmation of a price change can only be tested with a PayPal subscription, which depends on the payment methods available in Test Mode.
For a local test, expose the development server through a tunnel and use Simulate event on a Test Mode subscription. A simulated event for a subscription that was not created through a Vuesion checkout carries no billing reference, so Vuesion rejects it unless the subscription is already known locally. The complete flow for a new subscription requires a Test Mode checkout started from Vuesion.
Vuesion runs its maintenance tasks with Nitro scheduled tasks (nitro.scheduledTasks in nuxt.config.ts): the cleanup tasks daily at 03:00 and subscription-reconciliation hourly. Nitro runs the schedule inside the server process with the dev, node-server, bun, and deno-server presets, so the deployment needs a continuously running server process; for other presets, trigger the tasks externally. Every running instance runs the schedule; the tasks are safe to run more than once. The development server runs the schedule as well, and its /_nitro/tasks/:name endpoint runs a task manually, e.g. /_nitro/tasks/subscription-reconciliation. Every reconciliation run lists the provider subscriptions of the configured store to detect unknown subscriptions; the demo provider makes no requests.
The seed data shipped with Vuesion describes Vuesion's own product: the initial Free plan (bootstrapDefaultPlan in src/server/domain/plan/use-plan-service.ts, run by prisma/seed.ts) and the hosted demo in src/server/demo/seed-demo-data.ts with the Pro and Business plans, their monthly and yearly prices with fictional External IDs, and the demo users and workspace. Before launching your own product, review and replace:
The seed is code rather than environment configuration because it defines the initial product model of your application. Environment variables are reserved for deployment-specific values and secrets.
The demo prices use fictional External IDs such as demo_variant_pro_monthly. The public demo simulates billing, so they reference no Lemon Squeezy variant and need no Lemon Squeezy store or credentials. Configure the real products and variants of a production application separately, as plan prices with the variant IDs of its own Lemon Squeezy store. Test Mode and Live Mode have separate products and IDs, so production needs the variant IDs of the Live Mode store.
Several environment variables customize the product branding without modifying the application itself.
Examples include:
This allows the same application to be reused across different products while maintaining consistent branding.
The central application configuration lives inside:
nuxt.config.ts
This file configures:
Whenever possible, Vuesion follows the standard Nuxt configuration patterns instead of introducing project-specific alternatives.
Values required by the browser are exposed through Nuxt's runtime configuration.
Server-only values remain private.
This separation prevents secrets from accidentally becoming available on the client while still allowing browser code to access public configuration when necessary.
NUXT_PUBLIC_DEMO_MODE=true marks a deployment as a dedicated, disposable public demo. It defaults to false. A product with a public demo runs it as a separate deployment of the same build:
Production Public demo
app.example.com demo.example.com
production database dedicated, disposable database
NUXT_PUBLIC_DEMO_MODE=false NUXT_PUBLIC_DEMO_MODE=true
NUXT_RUN_STARTUP_TASKS=true
Never enable demo mode against a production database containing real user data. Demo mode does not isolate data. It deletes all application data of its database on every start, and the shared platform admin account can see and change everything in that database. The dedicated database is the only isolation.
Demo mode is a runtime setting, so the same build can run as a normal or a demo deployment. Only the value true enables it. Read it with isDemoModeEnabled(useRuntimeConfig().public.demoMode) from #shared/utils/demo-mode rather than checking the value for truthiness.
Two separate concepts are involved:
Deployment
└── demoMode this deployment is a disposable public demo
User
└── isDemo this user is a shared, system-provided demo identity
A visitor who registers on a demo deployment is a normal user with isDemo = false. isDemo is independent of isAdmin and is not an authorization concept.
isDemo protects the authentication identity of a shared demo user, so the shared credentials stay usable for everyone. It does not restrict normal application functionality. A demo user cannot change its email or password, connect or remove sign-in methods, or delete itself; these attempts fail with 403 This action is unavailable for shared demo accounts. Everything else works as for any other user. The protection depends only on isDemo, not on demo mode, and is enforced by the user service (canChangeUserIdentity in user.policy.ts).
Application and domain code should not branch on demo mode or isDemo. Application-specific demo content belongs in the demo fixture rather than in domain services. The only exception is billing, which demo mode simulates at the billing provider boundary (see below).
A demo deployment uses a dedicated, disposable database. With NUXT_RUN_STARTUP_TASKS=true and demo mode enabled, every application start runs the demo-lifecycle task:
deployment: npm run db:migrate-deploy && npm run db:seed
│
▼
application start (startup tasks)
│
▼
reset disposable application data src/server/demo/reset-demo-data.ts
│
▼
restore the initial default plan usePlanService().bootstrapDefaultPlan()
│
▼
seed the demo fixture src/server/demo/seed-demo-data.ts
The reset deletes all users, workspaces (including their subscriptions), plans, plan prices, incidents, and authentication tokens, including everything visitors created. Only the Feature table, which is synchronized from the code feature catalog, and the email rate limit counters in RateLimit survive. Uploaded files are marked as deleted; the file-cdn-cleanup task, which runs right after the lifecycle and daily, removes them from storage in batches of 100.
The task checks demo mode itself before deleting anything. A PostgreSQL advisory lock makes sure that only one instance runs the lifecycle when several start at the same time. Every later start, for example a restart or a new instance, resets the demo again. If the reset or the fixture fails, the task fails and the error is logged with the other startup task results.
Normal application features do not need demo mode handling. If a new feature needs representative sample data in the public demo, add that data to the demo fixture. If it introduces persisted disposable data that is not already removed through existing cascades, extend the explicit demo reset boundary in reset-demo-data.ts.
The provided fixture in src/server/demo/seed-demo-data.ts creates the data of the hosted Vuesion demo:
Plans
├── Free default, maxMembers = 3, supportLevel = COMMUNITY, no prices
├── Pro maxMembers = 10, supportLevel = PRIORITY
│ EUR 19.00 / month and EUR 190.00 / year (fictional External IDs)
└── Business maxMembers = unlimited, supportLevel = DEDICATED
EUR 49.00 / month and EUR 499.00 / year (fictional External IDs)
Users
├── demo@vuesion.dev / demo regular user
└── admin@vuesion.dev / admin platform admin
Workspaces
├── a personal workspace for each user, on Free
└── Acme Inc. owned by the regular user, on Free
Incidents (keys prefixed with demo:)
├── Unknown provider subscription billing ERROR 3 occurrences
├── Subscription workspace not found billing CRITICAL 1 occurrence
├── Subscription conflict billing WARNING 2 occurrences
├── Invalid order status transition orders ERROR 5 occurrences
└── External integration configuration missing integrations INFO 1 occurrence
The five incidents fill the incident board at /admin/incidents for the platform admin. They are fictional examples with different severities, sources, and diagnostic contexts; no subscription, order, or integration exists for them. The fixture reports them through reportIncident(), so repeated reports produce the occurrence counts. A resolved demo incident stays resolved until the next demo reset recreates it.
When demo mode is enabled, the sign-in dialog on the homepage offers one-click access with Continue as Demo User (demo@vuesion.dev / demo) and Continue as Platform Admin (admin@vuesion.dev / admin). Both use the regular email and password login. Below them, visitors can use their own email to go through the normal sign-up and authentication flow. The shortcuts are rendered by DemoLogin on the homepage only; the reusable AuthForm is not aware of demo mode.
Both users are shared demo identities (isDemo). Their credentials are intentionally public: the demo database is disposable, the reset restores it, and the identity protection keeps the shared sign-in usable.
The public demo simulates billing, so visitors can try plans and subscriptions without payment details and nothing reaches Lemon Squeezy:
Acme Inc. on Free → Plan & Billing → Pro or Business price
→ /workspaces/:id/demo-checkout (plan, price, "simulated, no payment information required")
→ Complete purchase → POST /api/workspaces/:id/billing/demo-checkout
→ simulated ACTIVE subscription → syncSubscription()
→ Acme Inc. on the purchased plan → entitlements updated
Only the provider is replaced; the billing domain stays the same:
useBillingProviderService() returns the simulated provider (use-demo-billing-provider-service.ts) instead of the Lemon Squeezy provider when demo mode is enabled. It reads the simulated subscription from the local Subscription, makes no HTTP requests, and reads no billing configuration, so a demo deployment needs no Lemon Squeezy credentials.demo_variant_<plan>_<monthly|yearly>), and simulated subscriptions and customers get generated demo-subscription-… and demo-customer-… IDs. The demo contains no real Lemon Squeezy identifiers or checkout URLs.OWNER, no current subscription) and then synchronizes a new ACTIVE subscription through syncSubscription(), the same projection that the webhook uses. The browser only chooses the plan price.POST .../billing/checkout) returns 404, the Lemon Squeezy webhook ignores every request, and reconciliation only re-applies the local state.Simulated subscriptions never expire on their own, and the simulation has no payment problems (PAST_DUE, UNPAID, PAUSED).
The provided fixture demonstrates Vuesion itself. Applications built from Vuesion should replace or extend the fixture with representative data for their own product. Normal domain services should remain unaware of demo mode.
Most development tools manage their own configuration.
Examples include:
eslint.config.ts
stylelint.config.mjs
vitest.config.mts
playwright.config.ts
prisma.config.ts
content.config.ts
Each tool follows its own standard configuration format.
Keeping configuration close to the corresponding tool makes updates easier and avoids introducing another abstraction layer.
Vuesion intentionally follows the conventions of the underlying ecosystem.
Instead of introducing custom configuration formats, the project uses the configuration mechanisms already established by:
Developers familiar with these tools should therefore immediately recognize where configuration belongs.
A typical local setup requires only a small number of environment variables:
Additional integrations such as email delivery, OAuth providers, or cloud storage can be configured when they become relevant for the project.
This keeps the first local setup straightforward while allowing production environments to enable additional capabilities.
Vuesion does not prescribe a particular deployment platform.
Because it is built on Nuxt, it can be deployed anywhere supported by Nuxt.
Deployment strategies differ significantly between projects and are therefore intentionally left outside the scope of this documentation.
Refer to the official Nuxt deployment documentation for platform-specific guidance.
Configuration should feel familiar.
Rather than introducing another layer of project-specific configuration, Vuesion builds upon the conventions already established by the tools it uses.
This provides:
The result is a project that feels like a well-organized Nuxt application rather than a framework built on top of Nuxt.
Continue with Database to learn how Vuesion organizes its Prisma schema, migrations, and data model.