# Introduction Nuxt Customer Portal is an MIT-licensed collection of Nuxt layers for organizations that need a secure place to work with customers, suppliers, and internal teams. It provides the shared foundation—authentication, organizations, roles, administration, navigation, and a dashboard—while business workflows are added as optional packages. ::warning The repository is licensed under MIT. Before depending on the alpha packages, read the current [licensing and compatibility status](https://nuxt-customer-portal.com/reference/compatibility-and-releases). :: That division is the project’s main design choice: - **Portal core** owns stable contracts and cross-cutting infrastructure. - **Feature layers** own complete business capabilities, from pages to database tables. - **The host application** stays thin and composes the installed layers. ::note The current timesheets feature is used for real operational work. It includes weekly entry, internal and client approvals, supplier workflows, reporting, and an invoices module with PDF and email delivery. :: ## Choose your path ::card-group :::card --- icon: i-lucide-rocket title: Run it locally to: https://nuxt-customer-portal.com/getting-started/installation --- Install Customer Portal, configure PostgreSQL, and start the development server. ::: :::card --- icon: i-lucide-waypoints title: Understand the architecture to: https://nuxt-customer-portal.com/architecture/overview --- Learn how portal core, the host, and feature layers fit together. ::: :::card --- icon: i-lucide-library title: Look up a contract to: https://nuxt-customer-portal.com/reference --- Find configuration, feature-registry fields, OpenAPI discovery, and compatibility guidance. ::: :::card --- icon: i-lucide-clock-3 title: Explore the modules to: https://nuxt-customer-portal.com/modules/timesheets-invoices --- See how the production timesheets and invoices workflow is assembled. ::: :::card --- icon: i-lucide-blocks title: Build a feature layer to: https://nuxt-customer-portal.com/contributing/create-a-layer --- Add a self-contained module without coupling it to the host application. ::: :::card --- icon: i-lucide-palette title: Brand a deployment to: https://nuxt-customer-portal.com/getting-started/customization --- Replace the reference identity, public content, themes, and email branding. ::: :: ## What the base portal includes - Email/password authentication, email verification, password recovery, and optional GitHub or Google OAuth. - Organizations, memberships, invitations, active-organization selection, and owner/admin/member roles. - System-administrator tools for managing users and organizations. - A feature registry for navigation, module menus, dashboard widgets, and typed policies. - PostgreSQL with Drizzle ORM and dependency-ordered provider migration streams. - English and Dutch localization with messages owned by each layer. - A Docker-ready Nuxt application with frontend and server routes in one service. ## Current feature layers | Layer | Purpose | | ------------------ | ------------------------------------------------------------------------------------ | | `core` | Headless sessions, authorization, database access, registry, and extension contracts | | `ui` | Neutral shell primitives, fallback layouts, dashboards, and surface rendering | | `authentication` | Sign in, sign up, verification, and password recovery | | `organizations` | Account settings, organizations, members, and invitations | | `administration` | System-administrator user and organization management | | `preset` | Core platform composition for common installations | | `kit` | Configuration, diagnostics, migrations, and legacy adoption | | `service-requests` | A compact reference feature for tenant-scoped request workflows | | `timesheets` | Time entry, approvals, suppliers, reporting, and invoices | Public pages, branding, and the application shell belong to the consuming host, as demonstrated independently by Apex and Brutal. The source is available on [GitHub](https://github.com/ludulicious/customer-portal){rel=""nofollow""}. If you want to contribute, start with the [contribution guide](https://nuxt-customer-portal.com/contributing). # Installation ## Requirements - a current Node.js LTS release; - Nuxt 4; - PostgreSQL; - pnpm, npm, Yarn, or Bun. ## Install the preset The preset contains core, the neutral UI fallback, authentication, organizations, and administration. Add optional business packages explicitly. ```bash [pnpm] pnpm add @nuxt-customer-portal/preset @nuxt-customer-portal/kit pnpm add @nuxt-customer-portal/service-requests @nuxt-customer-portal/timesheets ``` ```bash [npm] npm install @nuxt-customer-portal/preset @nuxt-customer-portal/kit npm install @nuxt-customer-portal/service-requests @nuxt-customer-portal/timesheets ``` ```bash [Yarn] yarn add @nuxt-customer-portal/preset @nuxt-customer-portal/kit yarn add @nuxt-customer-portal/service-requests @nuxt-customer-portal/timesheets ``` ```bash [Bun] bun add @nuxt-customer-portal/preset @nuxt-customer-portal/kit bun add @nuxt-customer-portal/service-requests @nuxt-customer-portal/timesheets ``` The packages are currently versioned together as `0.1.0-alpha.0`. The first repository milestone pack-tests these packages but does not publish them; use monorepo tarballs until the public prerelease is announced. ## Compose the layers ```ts [portal.config.ts] import { definePortalConfig } from '@nuxt-customer-portal/kit' export default definePortalConfig({ layers: [ '@nuxt-customer-portal/preset', '@nuxt-customer-portal/service-requests', '@nuxt-customer-portal/timesheets' ] }) ``` ```ts [nuxt.config.ts] import portal from './portal.config' export default defineNuxtConfig({ extends: portal.nuxtLayers }) ``` ## Configure the environment Generate and retain a unique Better Auth secret: ```bash [Terminal] openssl rand -base64 32 ``` Set at least: ```dotenv [.env] DATABASE_URL=postgresql://postgres:postgres@localhost:5432/customer_portal PUBLIC_URL=http://localhost:3000 BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET= PORTAL_REGISTRATION_MODE=open ``` ## Validate and migrate ```bash [Terminal] npx nuxt-customer-portal doctor npx nuxt-customer-portal db status npx nuxt-customer-portal db migrate ``` `doctor` detects duplicate providers, missing dependencies, and dependency cycles before SQL runs. Migrations execute in dependency order under a PostgreSQL advisory lock. Review package and local-layer migrations before applying them to an existing database. Start Nuxt with your package manager's normal development command. Next, review [usage](https://nuxt-customer-portal.com/getting-started/usage), [shell customization](https://nuxt-customer-portal.com/getting-started/customization), or [database migrations](https://nuxt-customer-portal.com/architecture/database-migrations). # Configuration Customer Portal reads its deployment configuration from environment variables. Start from `.env.example` and keep secrets out of version control. This page explains the main choices; use the [configuration reference](https://nuxt-customer-portal.com/reference/configuration) for every variable, default, precedence rule, and production check. ## Required settings | Variable | Purpose | | -------------------- | ----------------------------------------------------- | | `DATABASE_URL` | PostgreSQL connection string used by Drizzle | | `PUBLIC_URL` | Public application URL used in links and redirects | | `BETTER_AUTH_URL` | Base URL used by Better Auth on the server and client | | `BETTER_AUTH_SECRET` | High-entropy secret used to protect Better Auth state | Use HTTPS URLs for `PUBLIC_URL` and `BETTER_AUTH_URL` in production. Generate `BETTER_AUTH_SECRET` independently for each environment with `openssl rand -base64 32`. Do not put it in client runtime configuration, reuse a documentation value, or rotate it without a tested session-migration plan. ## Registration policy Set `PORTAL_REGISTRATION_MODE` to one of: - `open` — visitors may create accounts. - `invitation-only` — registration requires an organization invitation. - `disabled` — public registration is unavailable. `PORTAL_TERMS_URL` controls the Terms of Service link shown by the authentication forms. ## System administrators `ADMIN_EMAILS` is a comma-separated list of accounts that receive the system administrator role. Organization roles are separate: an organization can have owners, administrators, and members without granting system-wide access. ```dotenv [.env] ADMIN_EMAILS=owner@example.com,maintainer@example.com ``` ## Social authentication GitHub and Google sign-in are optional. Enable only providers for which credentials are configured: ```dotenv [.env] PORTAL_GITHUB_ENABLED=true GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= PORTAL_GOOGLE_ENABLED=true GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` Register the production and local callback URLs in the provider dashboard. If a provider is not needed, set its `PORTAL_*_ENABLED` flag to `false`. ## Email delivery Platform authentication messages use the platform Resend credentials: ```dotenv [.env] RESEND_API_KEY= RESEND_FROM_EMAIL= ``` Feature-specific transactional email can have different ownership. For example, invoice delivery credentials are configured per organization by the timesheets feature. ## Feature configuration Official and local layers are declared explicitly in `portal.config.ts`. `definePortalConfig()` converts them into the ordered `nuxtLayers` consumed by Nuxt `extends`; `localPortalLayer()` also records database-provider metadata. Disabling a layer removes its routes and registrations, but it does not delete its database schema or stored data. See [Nuxt layers](https://nuxt-customer-portal.com/architecture/layers). # Deployment Customer Portal builds as a single Nuxt service backed by PostgreSQL. The repository includes a multi-stage Dockerfile, but the generated Node server can also run on any platform that supports persistent environment variables and outbound PostgreSQL connections. ## Production requirements Prepare these before a release: - a PostgreSQL database with backups and a restricted application user; - HTTPS at the public origin; - production values for every required [environment variable](https://nuxt-customer-portal.com/getting-started/usage); - one stable, high-entropy `BETTER_AUTH_SECRET` shared by every instance in the deployment; - email credentials if registration, verification, recovery, or invoice delivery is enabled; - an explicit migration step using the same source revision as the application. The repository includes an ApexPro reference identity. Complete the [branding checklist](https://nuxt-customer-portal.com/getting-started/customization) before publishing a derived deployment. `PUBLIC_URL` and `BETTER_AUTH_URL` must both use the externally visible HTTPS origin. OAuth callback URLs must match that origin in GitHub or Google. ## Deploy with Docker Build the image from the repository root: ```bash [Terminal] docker build -t customer-portal . ``` Run it with production configuration supplied by the deployment platform: ```bash [Terminal] docker run --env-file .env.production -p 3000:3000 customer-portal ``` The included entrypoint applies Drizzle migrations before starting `.output/server/index.mjs`. A migration failure stops the container instead of serving code against an outdated schema. ::warning Do not bake `.env.production` into the image or commit it. Use the secret and environment-variable facilities of the hosting platform. :: ## Deploy the generated server For a Node deployment without the repository Dockerfile: ```bash [Terminal] pnpm install --frozen-lockfile pnpm build pnpm exec drizzle-kit migrate node .output/server/index.mjs ``` Apply the migration once as a release job before directing traffic to the new application version. Keep the migration files and application build from the same commit. ## Release sequence 1. Create or verify a production database backup and confirm the [restore procedure](https://nuxt-customer-portal.com/operations/backup-and-restore). 2. Build and validate the exact revision to deploy. 3. Review pending SQL, especially drops, rewrites, backfills, and cross-schema foreign keys. 4. Apply the ordered migration history. 5. Start the new application revision. 6. Verify sign-in, active-organization selection, a protected page, and any changed feature workflow. 7. Inspect server logs and email-provider delivery status after the release. Prefer backward-compatible schema changes when old and new instances may overlap during a rolling deployment. Learn how the repository manages changes in [database migrations](https://nuxt-customer-portal.com/architecture/database-migrations). If a migration or release fails, preserve its state before retrying and follow the [upgrade recovery runbook](https://nuxt-customer-portal.com/operations/upgrade-recovery). Establish the production signals described in [observability](https://nuxt-customer-portal.com/operations/observability) before the first release. # AI and machine-readable documentation Customer Portal publishes the same maintained documentation in formats intended for search, coding agents, and other AI tools. These endpoints supplement the rendered site; the Markdown files remain the source of truth. ## Raw Markdown Every maintained page has a raw representation under `/raw`: ```text Rendered: https://nuxt-customer-portal.com/architecture/layers Markdown: https://nuxt-customer-portal.com/raw/architecture/layers.md ``` The **Copy page** menu can copy either the page content or its canonical Markdown URL. ## llms.txt Use the generated indexes when a tool can read URLs but does not support MCP: - [`/llms.txt`](https://nuxt-customer-portal.com/llms.txt){rel=""nofollow""} provides the documentation map and descriptions. - [`/llms-full.txt`](https://nuxt-customer-portal.com/llms-full.txt){rel=""nofollow""} provides the maintained documentation as one combined context. Prefer the smaller index first, then load only relevant pages. Use the full file for cross-cutting architecture or contributor questions that genuinely need the entire documentation set. ## MCP server The documentation MCP server is available at: ```text https://nuxt-customer-portal.com/mcp ``` Its read-only `find-customer-portal-docs` tool searches maintained pages and returns their canonical rendered and Markdown URLs. An empty query lists the catalog. Every response also identifies the immutable public Customer Portal commit against which the documentation was checked. An agent should keep that revision with any extracted code or behavioral claim instead of assuming the moving `master` branch still matches. The server does not modify a Customer Portal deployment, access tenant data, or expose application administration tools. It only helps clients discover public documentation. ## Source fidelity Product behavior changes quickly. Documentation pull requests should verify public routes, environment variables, policies, and workflows against the current [Customer Portal source](https://github.com/ludulicious/customer-portal){rel=""nofollow""}. The revision shown on each rendered page and returned by MCP is the public source baseline for the current site. The site’s automated checks keep the MCP catalog aligned with maintained page titles, descriptions, and routes. # Customize and brand the portal Reusable packages deliberately contain no customer brand or public marketing site. Your Nuxt host owns `app.vue`, layouts, headers, navigation, footer, error page, global CSS, public pages, and assets. ## Start from the neutral fallback `@nuxt-customer-portal/ui` provides a complete neutral fallback layout, dashboard aggregation, menus, notifications, and shell primitives. The preset therefore works before you create a custom shell. Override a Nuxt layout or component in the host when you are ready; feature packages continue to contribute through the registry. The repository demonstrates two independent compositions: - `apps/demo-apex`: fixed header, collapsible sidebar, and restrained SaaS styling; - `apps/demo-brutal`: high-contrast editorial grid, module command bar, and two-pane responsive navigation. They enable the same preset, Service Requests, and Timesheets packages but do not share demo-only shell components. ## Keep the host boundary clear Place these in the application, not a reusable package: ```text app/app.vue app/layouts/ app/components// app/assets/css/ app/error.vue public/ ``` Use `PortalFeatureDefinition` navigation and dashboard registrations instead of importing an optional feature's Vue files. Render additional context panels through `PortalSurfaceContribution`; for example, Timesheets contributes to `administration.organization.detail` without Administration importing Timesheets. ## Brand runtime email Set the email subject brand in host runtime configuration and replace or wrap the neutral HTML template if your deployment needs a branded email frame: ```ts [nuxt.config.ts] export default defineNuxtConfig({ runtimeConfig: { portalEmail: { brandName: 'Example Company' } } }) ``` Exercise verification, sign-in, recovery, invitation, deletion, and invoice delivery against a controlled mailbox before deployment. ## Validate distinct shells Test authentication, organization switching, administration, enabled business features, keyboard navigation, contrast, and responsive behavior. A custom shell may change structure and presentation, but must preserve route authorization and active-organization semantics supplied by core. # Architecture overview Customer Portal is a Nuxt application assembled from layers. The architecture keeps shared infrastructure stable while allowing business capabilities to evolve independently. ## Three responsibilities ### The host The host is intentionally small. It owns the Nuxt entry point, branding, public site, layouts, error handling, and global assets. Migration order comes from installed provider manifests rather than a host-owned combined journal. ### Portal core `packages/core` is visually headless. It owns: - session and active-organization state; - authorization helpers and database access; - the feature and surface registry; - shared types for navigation, modules, widgets, audiences, and policies. `packages/ui` owns neutral fallback layouts, dashboard rendering, menus, modals, notifications, and shell primitives. Hosts can override its presentation without replacing core infrastructure. ### Feature layers A feature layer owns a vertical slice of the product. It may contribute pages, components, composables, server routes, translations, shared types, tests, and tables in its own PostgreSQL schema. ```text Host application ├── core │ ├── session, tenancy, and authorization │ ├── database adapters │ └── feature and surface registry ├── ui │ └── neutral fallback shell └── feature layers ├── service-requests └── timesheets ├── timesheets module └── invoices module ``` ## Runtime composition Each feature registers a `PortalFeatureDefinition` through a Nuxt plugin. Core and UI collect those definitions and derive: - global navigation; - module-aware sidebars based on the current route; - dashboard widgets grouped by area and order; - role policies used by server authorization. This means a feature can be installed or removed without adding imports to the shell. ## Data ownership Portal core and authentication tables live in PostgreSQL’s `public` schema. Each business feature owns a schema derived from its layer name, such as `timesheets` or `service_requests`. Each database-backed package owns an immutable migration stream. The kit orders provider streams while keeping a separate journal table for each provider. Continue with [feature layers](https://nuxt-customer-portal.com/architecture/layers), [core contracts](https://nuxt-customer-portal.com/architecture/core-contracts), or [tenancy and security](https://nuxt-customer-portal.com/architecture/tenancy-and-security). # Nuxt layers Every official package is a Nuxt layer whose package entry point is `nuxt.config.ts`. Each config supplies a stable `$meta.name`; code inside a layer may use that named Nuxt alias, while cross-package code uses documented package exports. ## Official composition `@nuxt-customer-portal/preset` includes `core`, `ui`, `authentication`, `organizations`, and `administration`. Add `@nuxt-customer-portal/service-requests` and `@nuxt-customer-portal/timesheets` independently. ```ts [portal.config.ts] import { definePortalConfig } from '@nuxt-customer-portal/kit' export default definePortalConfig({ layers: [ '@nuxt-customer-portal/preset', '@nuxt-customer-portal/timesheets' ] }) ``` ```ts [nuxt.config.ts] import portal from './portal.config' export default defineNuxtConfig({ extends: portal.nuxtLayers }) ``` ## Public imports Stable integration paths include: - `@nuxt-customer-portal/core/feature` for feature and surface contracts; - `@nuxt-customer-portal/core/server` for tenant-aware server adapters; - `@nuxt-customer-portal/core/schema` for official core schema exports; - each business package's `feature`, `types`, `schema`, and `portal-manifest` exports. The former `#portal`, `#types`, filesystem `#layers/*`, and subtree paths are intentionally unsupported. Packages resolve their own CSS and assets relative to `import.meta.url`. ## Local layers Use `localPortalLayer()` for deployment-owned features. A local layer has the same manifest semantics as official providers, including dependencies and immutable migrations: ```ts [portal.config.ts] localPortalLayer({ id: 'acme-billing', source: './layers/acme-billing', schema: './layers/acme-billing/server/db/schema', migrations: './layers/acme-billing/migrations', dependsOn: ['core'] }) ``` Removing a layer from configuration disables code only. It never removes stored data. # Portal-core contracts `PortalFeatureDefinition` is the public contract a feature registers with portal core. ```ts interface PortalFeatureDefinition { id: string navigation?: readonly PortalNavigationItem[] modules?: readonly PortalModuleContribution[] dashboardWidgets?: readonly PortalDashboardWidget[] surfaces?: readonly PortalSurfaceContribution[] policy: PortalFeaturePolicy } ``` ## Audiences Contributions declare who may see them: | Audience | Meaning | | ------------------- | ----------------------------------- | | `public` | No authenticated session required | | `authenticated` | Any signed-in user | | `organizationAdmin` | Organization owner or administrator | | `admin` | System administrator | Visibility is not authorization. Server routes must still enforce the feature policy. ## Navigation and modules `navigation` contributes top-level destinations. `modules` describe a cohesive area with: - a landing route; - route prefixes used to detect the active module; - an audience and order; - a module-specific sidebar menu. One feature may contribute multiple modules. The timesheets feature, for example, contributes separate **Timesheets** and **Invoices** modules while sharing domain data and policies. ## Dashboard widgets A widget declares a registered component plus its placement: - area: `attention`, `main`, or `aside`; - size: `full`, `half`, or `third`; - numeric order. The UI package sorts all installed widgets, so the dashboard does not import feature components directly. Components are registered by serializable name. ## Surface contributions Surfaces let an optional package contribute a named panel to another package's screen. Timesheets registers an organization panel for `administration.organization.detail`; Administration renders registered panels without importing Timesheets. ## Typed policies Features define their own action vocabulary and map actions to organization roles: ```ts const actions = ['read', 'create', 'update', 'submit', 'approve'] as const const policy = { owner: actions, admin: actions, member: ['read', 'create', 'update', 'submit'] } ``` Server routes pass the policy and requested action to `requireFeatureAccess`. That adapter authenticates the request, resolves the active organization, checks membership, evaluates the role, and returns tenant context for the query. ## Registration behavior Feature IDs are unique. Registering the same ID again replaces the previous definition, which supports Nuxt development reloads without duplicate navigation or widgets. For every field and allowed value, see the [feature contract reference](https://nuxt-customer-portal.com/reference/feature-contract). For a complete layer workflow, continue with [Create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer). # Tenancy and security Customer Portal is organization-scoped. A signed-in user can belong to multiple organizations, but each request operates in the context of one active organization. ## Role model The portal distinguishes two levels of authority: - **System administrator** — manages the whole portal and its organizations. - **Organization role** — `owner`, `admin`, or `member` within one organization. Feature policies map organization roles to feature-specific actions. A system administrator receives the standard platform bypass. ## Tenant-scoped APIs Feature handlers should start with `requireFeatureAccess`: ```ts import { requireFeatureAccess } from '@nuxt-customer-portal/core/server' import { exampleFeature } from '../../../shared/feature' export default defineEventHandler(async (event) => { const { session, organizationId } = await requireFeatureAccess( event, exampleFeature.policy, 'read' ) // Query using organizationId from the authenticated context. }) ``` Never trust an organization ID from a request body or query string when it can be derived from the session. Every tenant-owned query must include the resolved `organizationId`. ## Database separation Each feature owns a PostgreSQL schema. This reduces naming collisions and makes data ownership visible, but schema separation alone is not a tenancy boundary: rows must still be scoped to the active organization. Cross-schema foreign keys to core organizations and users are expected. Feature schemas should use deletion behavior deliberately so that organization cleanup cannot leave invalid records. ## Client-side visibility Audience-aware navigation improves the interface, but it does not secure data. Pages and buttons may be hidden for an unauthorized role; the corresponding server route must independently reject the request. ## Contribution checks Security-sensitive changes should include tests for: - unauthenticated access; - insufficient organization roles; - access with the wrong active organization; - system-administrator behavior; - destructive operations and their dependent records. # Database migrations Every database-backed official or local layer supplies an immutable `PortalLayerManifest`. The manifest identifies the provider, its package version, dependencies, schema export, and migration directory. Core, Service Requests, and Timesheets each ship a clean current-state baseline. ## Ordering and journals `nuxt-customer-portal db migrate` resolves all manifests, rejects duplicate IDs, cycles, missing dependencies, and incompatible duplicate versions, and then applies providers in dependency order. Execution is serialized with a PostgreSQL advisory lock. Each provider has its own journal table in the shared `nuxt_customer_portal_migrations` schema. A journal row records the migration filename, SHA-256 checksum, package version, and application time. An applied file whose contents later change is checksum drift and aborts the run before that SQL is replayed. ```bash [Terminal] npx nuxt-customer-portal doctor npx nuxt-customer-portal db status npx nuxt-customer-portal db migrate ``` Each migration file runs transactionally. A failure rolls back that file and releases the advisory lock; earlier committed provider migrations remain recorded and a repeat run resumes from the first pending file. ## Local providers Register a local layer with the same provider contract: ```ts [portal.config.ts] import { definePortalConfig, localPortalLayer } from '@nuxt-customer-portal/kit' export default definePortalConfig({ layers: [ '@nuxt-customer-portal/preset', localPortalLayer({ id: 'acme-billing', source: './layers/acme-billing', schema: './layers/acme-billing/server/db/schema', migrations: './layers/acme-billing/migrations', dependsOn: ['core'] }) ] }) ``` Generate only for a local provider; official package streams are immutable: ```bash [Terminal] npx nuxt-customer-portal db generate --provider acme-billing ``` Hosts should extend official data through host-owned tables in their own PostgreSQL schema. Direct changes to official tables require a fork or explicit takeover of that migration stream and fall outside package compatibility guarantees. ## Adopt a legacy installation The unchanged combined history is retained under `legacy/drizzle`. Adoption does not replay its SQL. First request a dry-run mapping: ```bash [Terminal] npx nuxt-customer-portal db adopt-legacy ``` The command verifies the recognized 22-entry legacy journal and representative core, Service Requests, and Timesheets tables. Any mismatch aborts without writing package journals. After reviewing the mapping, stamp the current package baselines: ```bash [Terminal] npx nuxt-customer-portal db adopt-legacy --apply ``` ## Disable or remove a provider Disabling a package removes its runtime contribution but never drops its tables or data. Permanent removal requires an explicit, host-owned migration and retention decision. This keeps package removal reversible and prevents configuration changes from becoming destructive database operations. # Module overview Customer Portal uses **layer** for a Nuxt package boundary and **module** for a cohesive product area shown in a shell. A layer can contribute one or more modules. ## Foundation packages | Package | Responsibility | | ---------------- | ---------------------------------------------------------------------------------- | | `core` | Headless session, tenancy, authorization, database, registry, and server contracts | | `ui` | Neutral components, surfaces, dashboards, menus, and fallback layouts | | `authentication` | Login, registration, verification, and password recovery | | `organizations` | Profiles, memberships, invitations, and organization settings | | `administration` | System-level users and organizations | | `preset` | Common platform composition | | `kit` | Configuration and provider migration tooling | [Understand the platform boundaries](https://nuxt-customer-portal.com/modules/platform-layers) ## Optional business packages ### Timesheets Timesheets contributes time entry, timers, projects, rates, internal and client approvals, reporting, sales invoices, received invoices, PDFs, email delivery, reminders, and payments. [Explore timesheets and invoices](https://nuxt-customer-portal.com/modules/timesheets-invoices) ### Service requests Service Requests is the smaller reference package. It demonstrates tenant-scoped APIs, typed policies, navigation, dashboard widgets, translations, tests, and a provider-owned PostgreSQL schema. [Explore service requests](https://nuxt-customer-portal.com/modules/service-requests) Start with Service Requests for a compact example. Study Timesheets for multiple module and surface contributions, complex approvals, document generation, and cross-organization access. # Timesheets and invoices The `timesheets` layer is a real operational module, not a demonstration scaffold. It combines weekly time entry with the review, reporting, and invoicing workflows needed around it. ## Two modules from one layer The layer registers two `PortalModuleContribution` entries. ### Timesheets The Timesheets module owns routes under `/timesheets` and `/admin/timesheets`. Its menu covers: - personal weekly time entry and timers; - client review and reviewer configuration; - supplier timesheets; - internal approvals and approval settings; - clients, projects, activities, rates, workspace settings, and reports. ### Invoices The Invoices module owns `/timesheets/invoices` and `/admin/timesheets/invoices`. Its menu separates received invoices, client invoice-viewer access, and organization-side sales invoices. Keeping both contributions in one layer allows them to share timesheet, client, organization, approval, and tariff data while remaining distinct destinations in the shell. ## Workflow capabilities ### Time entry Users can record time against projects and activities by week, use timers, update entries, and submit completed weeks. ### Approvals The feature supports internal approval rules and external client reviewers. Client access is scoped so reviewers see only the workspaces and supplier data they are allowed to review. ### Supplier collaboration Organizations can work with supplier timesheets and expose approval or invoice information to linked client organizations without granting broad portal access. ### Administration and reporting Organization administrators configure clients, projects, activities, team rates, capabilities, and workspace settings. Reports aggregate recorded time for operational and financial follow-up. The current reporting screen can also include draft, submitted, and rejected weeks unless status is filtered through the API. Follow [Report on recorded time](https://nuxt-customer-portal.com/guides/timesheet-reporting) before using totals for financial decisions. ### Invoicing The invoice workflow includes creation, numbering, line items, issue state, payment records, PDF generation, attachments, organization-specific sender details, email previews and delivery, reminders, and delivery status. ## Dashboard integration The layer contributes focused widgets rather than replacing the portal dashboard: - the user’s current week; - internal approvals needing attention; - client approvals; - supplier timesheets; - sales invoices; - received invoices. The UI layer places these widgets alongside contributions from other installed features. ## Permissions The typed action set is `read`, `create`, `update`, `submit`, `report`, `approve`, and `manage`. Owners and organization administrators receive every action. Members receive `read`, `create`, `update`, and `submit`; elevated workflows are checked by server handlers and capability-specific rules. ## Data ownership Feature-owned tables and enums live in the `timesheets` PostgreSQL schema. Cross-schema references connect them to core users and organizations. Timesheets owns an immutable migration stream that the kit applies after its declared dependencies. ## Extension lessons The layer is the best reference for: - contributing more than one module from one feature; - assembling route-aware module menus; - exposing data to another organization safely; - modeling multi-stage approvals; - generating and delivering documents; - splitting a large feature into domain components, composables, APIs, schemas, and shared types. To use the feature rather than study its implementation, start with [workspace setup](https://nuxt-customer-portal.com/guides/timesheet-setup), [time entry](https://nuxt-customer-portal.com/guides/time-entry), [approvals](https://nuxt-customer-portal.com/guides/approvals), or [invoicing](https://nuxt-customer-portal.com/guides/invoicing). # Service requests The `service-requests` layer is the smaller reference implementation for contributors. It owns its UI, API handlers, policies, translations, validation, types, tests, and Drizzle schema. ## Product surface Authenticated users can create and follow requests in their active organization. Organization administrators and system administrators receive a management view. The feature contributes: - user and administrator navigation; - a route-aware module menu; - attention and overview dashboard widgets; - tenant-scoped API routes; - tables and enums in the `service_requests` PostgreSQL schema. ## Policy The action vocabulary is `create`, `read`, `update`, `delete`, `list`, and `manage`. Owners and organization administrators receive all actions. Members can create, read, update, and list requests, but cannot perform management or destructive actions. ## Why it is the reference layer Service requests is small enough to read end to end while exercising every important extension point: 1. Define a typed feature and role policy. 2. Register navigation, a module, menu items, and dashboard widgets. 3. Implement pages and components inside the layer. 4. Protect server handlers through portal core. 5. Scope stored records to the active organization. 6. Own translations and shared types. 7. Test feature registration, policy, locale parity, and schema conventions. Use it alongside the [create-a-layer guide](https://nuxt-customer-portal.com/contributing/create-a-layer). The [service-request user guide](https://nuxt-customer-portal.com/guides/service-requests) documents the current product workflow and its organization-wide visibility. Move to the [timesheets feature](https://nuxt-customer-portal.com/modules/timesheets-invoices) when you need a larger example. # Platform layers Nuxt Customer Portal follows Nuxt's npm-layer model. Each layer package exposes `nuxt.config.ts`, declares a stable `$meta.name`, and is composed through `extends`. The preset provides a useful starting point without owning the host's brand or public website. ## Layer catalog | Package | Owns | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `core` | Sessions, Better Auth infrastructure, tenancy, authorization, database access, feature registry, shared contracts, and generic OpenAPI merging | | `ui` | Neutral shell primitives, contribution rendering, and fallback layouts | | `authentication` | Login, signup, email verification, and password recovery UI | | `organizations` | Profile, organization selection, invitations, settings, and email-provider credentials | | `administration` | Installation-wide organization and user administration | | `preset` | Composition of core, UI, authentication, organizations, and administration | | `kit` | Portal configuration, diagnostics, provider resolution, migrations, and legacy adoption | `service-requests` and `timesheets` are optional business packages. A host selects either, both, or neither in `portal.config.ts`. ## Core and UI `core` is visually headless. It must not render a branded application shell or import an optional business package. Feature code consumes `@nuxt-customer-portal/core/feature`, `@nuxt-customer-portal/core/server`, and `@nuxt-customer-portal/core/schema`. `ui` turns serializable feature registrations into neutral navigation, dashboards, surfaces, modals, and fallback layouts. A production host can override those layouts and compose the same primitives differently. The Apex and Brutal demos prove this by sharing package selection but not demo-only shell components. [Open the core and UI contracts](https://nuxt-customer-portal.com/reference/source-map#core-and-ui-contracts) ## Authentication and organizations `authentication` owns account entry and recovery screens while core retains Better Auth configuration and identity records. `organizations` owns the signed-in user's profile, organization selection, invitations, and tenant settings. Neither grants installation-wide administration privileges. Server operations always derive the active organization from the authenticated session. Organization identifiers supplied by the browser are not trusted when session context determines the tenant. ## Administration and surfaces `administration` owns system-administrator routes and APIs. Optional features must not be imported by administration to add feature-specific panels. Instead they register a `PortalSurfaceContribution` for a named surface such as `administration.organization.detail`; the UI resolves the component only when that package is installed. ## Preset and kit Use `@nuxt-customer-portal/preset` for the common platform. Add business packages independently. The kit converts `portal.config.ts` to Nuxt `extends`, resolves official and local manifests, checks dependency order, and manages one immutable migration stream per provider. Public pages, branding, `app.vue`, layouts, headers, footers, error pages, assets, and global styling are host-owned. They intentionally have no public-site package. ## Decide where a change belongs 1. Shared sessions, tenancy, authorization, database, registry, or contracts belong in core. 2. Reusable presentation and fallback shells belong in UI. 3. Account entry belongs in authentication. 4. Profile and tenant management belong in organizations. 5. Installation-wide management belongs in administration. 6. Optional domain workflows belong in their own package. 7. Brand and marketing experiences belong in a host app. If a change crosses boundaries, add the smallest serializable contract or surface registration instead of a private physical import. Continue with [Create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer) or inspect the [product source map](https://nuxt-customer-portal.com/reference/source-map). # Reference Use this section when you need the exact shape or current guarantee of a Customer Portal interface. The task guides explain what to do; these pages describe the configuration and contracts the current source actually exposes. ::card-group :::card --- icon: i-lucide-settings-2 title: Configuration reference to: https://nuxt-customer-portal.com/reference/configuration --- Environment variables, defaults, precedence, secrets, and deployment checks. ::: :::card --- icon: i-lucide-braces title: Feature contract reference to: https://nuxt-customer-portal.com/reference/feature-contract --- Exact registry fields for navigation, modules, widgets, audiences, and policies. ::: :::card --- icon: i-lucide-file-json-2 title: Server API and OpenAPI to: https://nuxt-customer-portal.com/reference/server-api --- Discover deployed endpoints and add authenticated, tenant-scoped feature handlers. ::: :::card --- icon: i-lucide-git-compare-arrows title: Compatibility and releases to: https://nuxt-customer-portal.com/reference/compatibility-and-releases --- Current project maturity, commit pinning, migrations, and safe upgrade expectations. ::: :: ::note Customer Portal is currently distributed as a GitHub application template, not as a versioned npm package. Reference pages therefore describe the checked-in contracts on `master`; review the compatibility page before upgrading a deployed portal. :: # Configuration reference Customer Portal reads deployment settings from the process environment. Copy `.env.example` for local development, but provide production values through the hosting platform or secret manager. ## Environment variables | Variable | Required | Default | Meaning | | -------------------------- | -------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DATABASE_URL` | Yes | None | PostgreSQL connection string used by the shared Drizzle client and migrations. | | `PUBLIC_URL` | Production | `http://localhost:3051` in `.env.example` | Public origin used to construct application links and as the fallback Better Auth origin. | | `BETTER_AUTH_URL` | Production | Falls back to `PUBLIC_URL` | Canonical Better Auth server and client origin. It must address the same deployed portal. | | `BETTER_AUTH_SECRET` | Yes | None in production | High-entropy secret used by Better Auth for encryption, signing, and hashing. Use at least 32 characters and keep the same value across application instances. | | `ADMIN_EMAILS` | No | Empty | Comma-separated, case-insensitive email allowlist for system administrators. Whitespace is trimmed. | | `RESEND_API_KEY` | Email flows | Empty | Platform Resend credential for authentication messages such as verification and password reset. | | `RESEND_FROM_EMAIL` | Email flows | Empty | Sender used with the platform Resend credential. | | `PORTAL_GITHUB_ENABLED` | No | `true` | Shows and configures GitHub sign-in when both GitHub credentials are also present. | | `GITHUB_CLIENT_ID` | GitHub sign-in | Empty | GitHub OAuth client identifier. | | `GITHUB_CLIENT_SECRET` | GitHub sign-in | Empty | GitHub OAuth client secret. | | `PORTAL_GOOGLE_ENABLED` | No | `true` | Shows and configures Google sign-in when both Google credentials are also present. | | `GOOGLE_CLIENT_ID` | Google sign-in | Empty | Google OAuth client identifier. | | `GOOGLE_CLIENT_SECRET` | Google sign-in | Empty | Google OAuth client secret. | | `PORTAL_REGISTRATION_MODE` | No | `open` | Account policy: `open`, `invitation-only`, or `disabled`. Invalid values fall back to `open`. | | `PORTAL_TERMS_URL` | No | `/` | Link shown from authentication forms for the deployment's Terms of Service. | The provider flags use strict string parsing: only the literal value `true` enables a flag. Set a provider flag to `false` when the deployment does not configure that provider. Generate `BETTER_AUTH_SECRET` with `openssl rand -base64 32`; never copy a documentation placeholder into a deployment. See the [Better Auth secret reference](https://better-auth.com/docs/reference/options#secret){rel=""nofollow""} for the upstream contract. ## Runtime variables These variables belong to Node or the generated Nitro server rather than a Customer Portal feature: | Variable | Production behavior | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `NODE_ENV` | Set to `production` by the Docker image and normal production runtimes. Customer Portal uses it to avoid retaining the development database singleton. | | `HOST` | Bind address for the generated server. The Docker image sets `0.0.0.0`; use a loopback address only when a local proxy is the sole caller. | | `PORT` | Listening port for the generated server. Nitro uses port `3000` unless the runtime overrides it. | ## URL precedence The authentication server resolves its origin in this order: 1. `BETTER_AUTH_URL`; 2. `PUBLIC_URL`; 3. the local development fallback. Set both URL variables to the same HTTPS origin in production. A mismatch can produce incorrect email links, rejected OAuth callbacks, or session cookies attached to the wrong host. Do not add a trailing path. ```dotenv [.env] PUBLIC_URL=https://portal.example.com BETTER_AUTH_URL=https://portal.example.com ``` ## Registration and administrators `PORTAL_REGISTRATION_MODE` controls who may create an account: | Value | Behavior | | ----------------- | --------------------------------------------------------------- | | `open` | Anyone can use the signup flow. | | `invitation-only` | Signup is available only in an organization invitation flow. | | `disabled` | Public signup is unavailable; existing users may still sign in. | `ADMIN_EMAILS` grants the system-wide administrator role when a listed account registers or signs in. This is separate from the `owner`, `admin`, and `member` roles inside an organization. Limit this list to accounts that genuinely administer the whole installation. ## Email ownership The Resend environment variables belong to the platform and send authentication mail. Organization-owned transactional mail is configured in the portal UI and stored per organization. The timesheets module uses that organization credential for invoices and reminders, so changing the platform Resend key does not replace an organization's sender. ## Secret handling Keep these server-only values out of source control, client runtime configuration, logs, screenshots, and issue reports: - `DATABASE_URL`; - `BETTER_AUTH_SECRET`; - `RESEND_API_KEY`; - `GITHUB_CLIENT_SECRET`; - `GOOGLE_CLIENT_SECRET`; - organization email-provider credentials. Client IDs and public origins are identifiers rather than secrets, but should still be specific to the intended deployment. Do not rotate `BETTER_AUTH_SECRET` by simply replacing it on running instances: existing encrypted or signed auth state may depend on the prior value. Plan secret rotation using Better Auth's versioned-secret mechanism and test active sessions and OAuth flows before production rollout. ## Production verification After changing configuration: 1. restart the application so Nitro reads the new environment; 2. open the portal at `PUBLIC_URL` and complete a sign-in; 3. verify signup matches the configured registration mode; 4. request an authentication email and inspect its origin and sender; 5. test each enabled OAuth provider from the public hostname; 6. confirm a normal member does not receive system-administrator access; 7. run the deployment checks in [Deployment](https://nuxt-customer-portal.com/getting-started/deployment). # Feature contract reference Feature layers integrate with the application shell through `PortalFeatureDefinition`. Import the type from `@nuxt-customer-portal/core/feature` and register a definition from a client plugin with `usePortalFeatures()`. ```ts interface PortalFeatureDefinition { id: string navigation?: readonly PortalNavigationItem[] modules?: readonly PortalModuleContribution[] dashboardWidgets?: readonly PortalDashboardWidget[] surfaces?: readonly PortalSurfaceContribution[] policy: PortalFeaturePolicy } ``` ## Feature fields | Field | Required | Contract | | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------- | | `id` | Yes | Stable, globally unique feature identifier. Registering the same ID again replaces the previous definition. | | `navigation` | No | Top-level destinations contributed to the portal shell. | | `modules` | No | Cohesive module areas and their sidebar menus. One feature may contribute multiple modules. | | `dashboardWidgets` | No | Components aggregated into the shared dashboard. | | `surfaces` | No | Generic component-name contributions to a documented host surface. | | `policy` | Yes | Allowed feature actions for each organization role. | The registry derives sorted `navigation`, `modules`, and `dashboardWidgets` collections. Navigation and modules default to order `100`; widgets require an explicit order. ## Audiences and roles ```ts type PortalAudience = | 'public' | 'authenticated' | 'organizationAdmin' | 'admin' type PortalOrganizationRole = 'owner' | 'admin' | 'member' ``` Audiences control shell visibility. Policies control server authorization. A hidden link does not protect an API route. ## Navigation item | Field | Type | Notes | | ----------- | ------------------- | -------------------------------------------------------------- | | `id` | `string` | Stable identifier within the registry. | | `labelKey` | `string` | Translation key, normally below `features.`. | | `icon` | `string?` | Nuxt Icon name. | | `to` | `string` | Destination route. | | `audiences` | `PortalAudience[]` | People who may see the destination. | | `location` | `'main' | 'admin'?` | Defaults to the main application navigation. | | `order` | `number?` | Lower values appear first; default is `100`. | | `badge` | `PortalBadgeValue?` | Package-owned serializable label, color, and variant contract. | ## Module contribution A module describes one selectable product area: | Field | Type | Notes | | --------------- | ------------------------- | ------------------------------------------------------ | | `id` | `string` | Stable module identifier. | | `labelKey` | `string` | Translated module label. | | `icon` | `string?` | Module icon. | | `to` | `string` | Landing route for the current user. | | `routePrefixes` | `string[]` | Prefixes that keep the module active while navigating. | | `audiences` | `PortalAudience[]` | Shell visibility. | | `order` | `number?` | Module ordering; default is `100`. | | `badge` | `PortalBadgeValue?` | Optional serializable module indicator. | | `menuItems` | `PortalModuleMenuItem[]?` | Sidebar destinations belonging to the module. | A menu item uses `id`, `labelKey`, `to`, `audiences`, and optional `icon`, `exact`, `order`, and `badge` fields. Use `exact: true` when a parent route must not remain active on child pages. ## Dashboard widget | Field | Allowed values | | ----------- | -------------------------------- | | `id` | Stable string identifier | | `component` | Registered component name string | | `area` | `attention`, `main`, or `aside` | | `size` | `full`, `half`, or `third` | | `order` | Required number | Widgets sort by area, then order, then ID. A feature may register an initial definition and replace it after loading server capabilities; the timesheets layer uses this to remove widgets and menu items a user cannot access. ## Surface contribution `PortalSurfaceContribution` has a unique `id`, a documented `surface`, a registered component-name string, and optional numeric `order`. The built-in cross-feature surface is `administration.organization.detail`. The contributing package owns the rendered component. ## Policy Define a literal action list and map every organization role deliberately: ```ts const actions = ['read', 'create', 'update', 'manage'] as const type Action = typeof actions[number] const policy: PortalFeaturePolicy = { owner: actions, admin: actions, member: ['read', 'create', 'update'] } ``` System administrators bypass an organization policy. Feature routes should normally call `requireFeatureAccess(event, policy, action)` from `@nuxt-customer-portal/core/server`. It returns the authenticated session and active organization ID after enforcing both membership and the policy. The server adapter also exposes `getSession`, `requireSession`, `requireActiveOrganization`, `authorize`, `hasFeatureAccess`, `requireActiveOrganizationRole`, and owner-only organization email-credential access. Prefer these adapters over importing Better Auth or core database internals into a feature. For an end-to-end implementation, continue with [Create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer). # Server API and OpenAPI Customer Portal uses Nitro file-based handlers. The assembled application generates its API reference from the handlers present in portal core and every enabled feature layer. ## Discover a deployed API Each running portal exposes: | Route | Purpose | | ---------------------------- | ----------------------------------------------------------------------------------- | | `/api-docs` | Scalar interactive API browser. | | `/api-docs/swagger` | Swagger UI for the same document. | | `/api-docs/openapi.json` | Ordered OpenAPI document enriched with Better Auth operations and shared contracts. | | `/api-docs/openapi.raw.json` | Nitro's unmerged generated document, intended for internal assembly. | Use the ordered `/api-docs/openapi.json` document for clients and reviews. It reflects the layers installed in that deployment, so it is more authoritative than a copied endpoint list. ::warning The interactive API uses the current browser session and its permissions. It is not an anonymous administration API, and successful calls still depend on the active organization and feature policy. :: ## Current product surface At the documented `master` revision, the assembled repository contains 111 Nitro API handler files. The count is a review signal rather than an API version: a catch-all handler can contribute multiple OpenAPI operations, and a handler can respond to one method only. | Owning layer | Handler files | Route families | Primary responsibility | | ------------------ | ------------: | --------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `core` | 3 | `/api/auth/**`, `/api/notifications` | Better Auth transport, current permissions, and notification feed | | `ui` | 0 | — | Neutral presentation only | | `authentication` | 0 | — | Uses core identity handlers and owns only account-entry UI | | `organizations` | 10 | `/api/organizations/**`, `/api/profile` | Profile, invitations, organization details, and email-provider credentials | | `administration` | 14 | `/api/admin/**` | Installation-wide users, organizations, memberships, invitations, and roles | | `service-requests` | 8 | `/api/service-requests/**` | Customer and organization-administrator request workflows | | `timesheets` | 76 | `/api/timesheets/**` | Entry, timers, setup, approvals, reporting, clients, invoices, PDFs, email, and cross-organization access | | `preset` | 0 | — | Composition only | | `kit` | 0 | — | Configuration and migration CLI only | CI compares these layer and handler counts with the pinned public repository tree. When the product source pin changes, a mismatch requires the inventory and relevant narrative documentation to be reviewed together. For the exact operations, parameters, and response schemas in a deployment, use its generated OpenAPI document. ## Authentication and tenancy Most product handlers are session APIs rather than token-based public APIs. A protected feature operation should: 1. require an authenticated session; 2. derive the active organization from that session; 3. evaluate a typed feature action against the user's organization role; 4. scope every tenant-owned query by the returned organization ID; 5. validate path, query, and body input before repository access. ```ts [layers/example/server/api/example/index.post.ts] import { definePortalRouteMeta, requireFeatureAccess } from '@nuxt-customer-portal/core/server' import { exampleFeature } from '../../../shared/feature' import { createExampleSchema } from '../../utils/example-validation' definePortalRouteMeta({ openAPI: { tags: ['Example'], operationId: 'exampleCreate', summary: 'Create an example record', description: 'Creates a record in the active organization.' }, body: createExampleSchema }) export default defineEventHandler(async (event) => { const { session, organizationId } = await requireFeatureAccess( event, exampleFeature.policy, 'create' ) const input = createExampleSchema.parse(await readBody(event)) return createExample({ ...input, organizationId, createdById: session.user.id }) }) ``` Do not accept `organizationId` from a normal browser request when the active session already determines the tenant. Administrative cross-organization operations require a separate, explicit system-administrator check. ## OpenAPI metadata Every contributor-owned handler should call `definePortalRouteMeta()` beside the route with: - one stable, unique `operationId`; - a domain tag such as `Timesheets` or `Service requests`; - an imperative summary; - a description that states authentication, tenant scope, and the relevant permission; - request and response schemas when Nitro cannot infer enough detail. Keep the metadata beside the handler so moving or removing a layer also moves or removes its API description. ## Error semantics Core adapters establish these baseline responses: | Status | Meaning | | ------ | ---------------------------------------------------------------------------- | | `400` | The session has no active organization or validated input is invalid. | | `401` | No authenticated user session. | | `403` | The user lacks organization membership or the required feature action. | | `404` | A scoped record does not exist or is not visible in the active organization. | | `409` | The requested transition conflicts with current domain state. | Feature repositories should avoid revealing whether a record exists in another organization. Validation failures should be useful to the caller without exposing secrets, SQL, or credentials. ## Stability The generated OpenAPI document describes the current deployment; it is not yet a versioned public API guarantee. Pin the Customer Portal commit used by an integration and review the document when upgrading. See [Compatibility and releases](https://nuxt-customer-portal.com/reference/compatibility-and-releases). # Compatibility and releases Nuxt Customer Portal is an MIT-licensed monorepo. Copyright belongs to “Nuxt Customer Portal contributors”; the repository root `LICENSE` governs the reusable packages and source unless a file says otherwise. The public packages use linked version `0.1.0-alpha.0`. This milestone pack-tests the package artifacts without publishing them. Alpha releases may make intentional breaking changes before `1.0.0`; Changesets records user-visible package changes and keeps the official versions aligned. ## Supported baseline - Nuxt 4 and compatible Vue/framework peers; - PostgreSQL; - English and Dutch; - npm, pnpm, Yarn, and Bun consumers. Documented package exports are the compatibility surface. Host aliases, physical paths across packages, demo shell components, raw internal records, and direct changes to official database tables are not. ## Upgrading 1. Read package Changesets and compare manifests. 2. Run `nuxt-customer-portal doctor`. 3. Inspect `nuxt-customer-portal db status` and the pending SQL. 4. Test a fresh database and an upgrade copy. 5. Run typecheck, builds, package tests, and critical portal workflows. 6. Deploy with a restorable backup and a forward-repair plan. Checksums make published migration files immutable. Disabling a package leaves its data intact. A host that alters an official table must fork or explicitly take over that provider's migration stream and cannot assume upstream compatibility. ## Publication boundary The first milestone does not publish npm packages, change DNS, migrate production databases, or modify Ludulicious. Registry scope ownership and package-name availability must be verified before a public prerelease. # Glossary Customer Portal uses several similar words for deliberately different concepts. Use these meanings in issues, documentation, code review, and module proposals so a discussion does not confuse packaging, product navigation, or authorization. ## Platform and composition ### Customer Portal The MIT-licensed Nuxt package family that combines a headless core, UI primitives, platform layers, and optional business layers into a deployable portal. ### Host application The Nuxt application that composes enabled layers and owns branding, public pages, `app.vue`, layouts, global styling, assets, and deployment configuration. The host should not import feature-private menus, pages, or server handlers. ### Core The visually headless foundational layer that owns sessions, active-organization context, authorization adapters, the feature registry, database access, and shared contracts. Business layers depend on core; core never depends on an optional feature. ### Platform layer A layer that provides reusable portal infrastructure rather than one optional business capability. UI, authentication, organizations, and administration are current platform layers; public sites are host-owned. ### Feature layer The source and packaging boundary for one coherent business capability. A feature layer can own pages, components, composables, plugins, API routes, translations, types, tests, and a PostgreSQL schema. `service-requests` and `timesheets` are feature layers. ### Module A cohesive product area contributed to the application shell. A module has a landing route, matching route prefixes, audiences, and an optional sidebar menu. One feature layer may contribute multiple modules; the timesheets layer contributes **Timesheets** and **Invoices**. ### Feature definition The typed `PortalFeatureDefinition` registered by a layer. It groups the feature ID, navigation, modules, dashboard widgets, and organization-role policy. ### Contribution One item a feature adds to a shared portal surface, such as a navigation link, module, module-menu item, dashboard widget, or named `PortalSurfaceContribution`. The UI aggregates contributions without importing the feature directly. ### Feature registry Core's runtime collection of feature definitions. Registration is idempotent by feature ID: registering the same ID again replaces its previous definition, enabling capability-filtered menus, widgets, and surfaces. ## Identity, tenancy, and authorization ### Active organization The organization selected in the authenticated session. Feature APIs derive their tenant boundary from this context instead of trusting an organization ID supplied by the browser. ### Tenant An organization whose records must remain isolated from other organizations. A tenant-owned query always includes the authorized organization boundary. ### Organization role Membership authority inside one organization: `owner`, `admin`, or `member`. A person can have a different role in another organization. ### Organization owner The highest organization role. Owners receive the feature actions granted to `owner` and may perform especially sensitive organization operations that ordinary organization administrators cannot. ### Organization administrator An organization member with the `admin` role. Administrators normally configure feature workspaces and manage organization workflows, but they are not automatically installation-wide system administrators. ### System administrator An installation-wide administrator identified by the platform’s system role. System administrators receive the standard feature-policy bypass and can administer users and organizations across the installation. ### Audience A shell-visibility category: `public`, `authenticated`, `organizationAdmin`, or `admin`. Audiences decide whether navigation is shown; they do not protect an API route. ### Policy A typed mapping from organization roles to feature actions. Each server route independently enforces the relevant action through core authorization adapters. ### Action A feature-defined authorization verb such as `read`, `create`, `submit`, `approve`, or `manage`. Actions describe business authority more precisely than a visible route or button. ### Capability A context-specific result that says whether a user can perform or see something in the active organization. Capabilities may combine role, feature policy, workspace settings, assignments, linked-organization access, and pending work. ### Cross-organization access Explicit access from one organization to narrowly scoped records owned by another. Client review, supplier timesheets, and received invoices use dedicated relationships rather than weakening the normal active-organization boundary. ## User interface contracts ### Navigation item A top-level destination contributed by a feature. It has a stable ID, translated label, route, audiences, location, order, and optional badge. ### Module menu The sidebar destinations for the currently active module. Route prefixes select the module; audiences and capabilities filter what the current user sees. ### Dashboard widget A feature-owned Vue component placed by the UI layer into `attention`, `main`, or `aside`. Its definition also declares size and deterministic order. An error boundary isolates each widget from the rest of the dashboard. ### Badge A static or capability-derived indicator on navigation, modules, or menu items. Counts communicate pending work but never replace server authorization. ## Data and API contracts ### Feature-owned PostgreSQL schema A PostgreSQL namespace containing a provider's tables and enums. Its name normally derives from the provider ID. Cross-schema foreign keys may reference core users and organizations. ### Provider migration stream An immutable migration directory owned by one official, local, or third-party provider. The kit validates checksums and applies provider streams in dependency order under a PostgreSQL advisory lock, with one journal table per provider. ### DTO A data-transfer object explicitly shaped for an API consumer. DTOs keep persistence details and private columns out of client responses. ### Server adapter A stable core function used by feature APIs for sessions, active organizations, authorization, organization members, organization lookup, and database access. It prevents runtime dependencies on private authentication or host implementations. ### OpenAPI contract Machine-readable metadata describing an API operation, request, response, and error behavior. Customer Portal serves the generated contract through its authenticated API documentation routes. ## Timesheets and invoices ### Timesheet workspace The active organization’s configuration for time entry, team members, clients, projects, activities, rates, approvals, reports, and invoicing. ### Supplier organization The organization that owns a timesheet workspace and supplies recorded work to a linked client organization. ### Client organization An organization linked to a supplier workspace for view, review, or invoice access. The link does not grant broad access to the supplier’s tenant data. ### Internal approval The supplier organization’s review of a submitted member week. Current configuration can enable the workflow, decide which members require it, and assign one or more eligible approvers. ### Client review A linked client organization’s decision on the portion of approved work relevant to it. Reviewers can approve or dispute according to explicit supplier-workspace assignments. ### Sales invoice An invoice created and managed by the supplier organization from approved, eligible time or manual lines. ### Received invoice The client-side view of an issued supplier invoice, available only to configured invoice viewers in the linked client organization. ## Distribution and documentation ### Source layer A host-owned feature layer referenced through `localPortalLayer()`. It uses the same manifest and provider model as official packages while remaining in the consuming application. ### Remote layer A Nuxt layer loaded from a pinned Git source through `extends`. Nuxt supports the mechanism; the layer author owns package compatibility, manifest resolution, and migration integration. ### Packaged layer A separately versioned Nuxt layer published to a package registry and loaded through `extends`. Official packages share an alpha version and expose explicit package entry points. ### Disable Stop composing a layer so its pages, APIs, contributions, and translations disappear while its stored data remains. Disabling code is intentionally separate from deleting data. ### Remove Permanently delete layer code and, only after an explicit retention decision, apply a reviewed migration that removes its database objects. ### Source pin The immutable Customer Portal commit against which a documentation version was checked. Every rendered page, documentation report, and MCP discovery response carries the same pin. ### Compatibility baseline The exact Customer Portal revision, runtime, database expectation, and layer version proven together by tests. A moving branch or the word `latest` is not a reproducible baseline. Continue with the [feature contract reference](https://nuxt-customer-portal.com/reference/feature-contract) for exact fields or use the [source map](https://nuxt-customer-portal.com/reference/source-map) to open their current implementations. # Product source map This map connects maintained documentation to the canonical monorepo. During the package-ready milestone the links follow `master`; release documentation should pin the corresponding release tag or commit. ## Workspace and demos - [`package.json`](https://github.com/ludulicious/customer-portal/blob/master/package.json){rel=""nofollow""} — private workspace commands and Changesets tooling. - [`pnpm-workspace.yaml`](https://github.com/ludulicious/customer-portal/blob/master/pnpm-workspace.yaml){rel=""nofollow""} — application and package membership. - [`LICENSE`](https://github.com/ludulicious/customer-portal/blob/master/LICENSE){rel=""nofollow""} — repository-wide MIT terms. - [`apps/demo-apex/portal.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/apps/demo-apex/portal.config.ts){rel=""nofollow""} — Apex package selection. - [`apps/demo-apex/nuxt.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/apps/demo-apex/nuxt.config.ts){rel=""nofollow""} — conventional SaaS host configuration. - [`apps/demo-apex/app/app.vue`](https://github.com/ludulicious/customer-portal/blob/master/apps/demo-apex/app/app.vue){rel=""nofollow""} — Apex host entry point. - [`apps/demo-brutal/portal.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/apps/demo-brutal/portal.config.ts){rel=""nofollow""} — Brutal package selection. - [`apps/demo-brutal/app/layouts/default.vue`](https://github.com/ludulicious/customer-portal/blob/master/apps/demo-brutal/app/layouts/default.vue){rel=""nofollow""} — independent editorial shell. - [`apps/demo-brutal/app/assets/css/main.css`](https://github.com/ludulicious/customer-portal/blob/master/apps/demo-brutal/app/assets/css/main.css){rel=""nofollow""} — high-contrast responsive styling. ## Core and UI contracts - [`packages/core/nuxt.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/nuxt.config.ts){rel=""nofollow""} — visually headless core layer. - [`packages/core/portal.manifest.mjs`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/portal.manifest.mjs){rel=""nofollow""} — core provider metadata. - [`packages/core/shared/types/feature.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/shared/types/feature.ts){rel=""nofollow""} — navigation, widget, surface, badge, and policy contracts. - [`packages/core/shared/feature-registry.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/shared/feature-registry.ts){rel=""nofollow""} — deterministic feature registration. - [`packages/core/shared/portal-session.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/shared/portal-session.ts){rel=""nofollow""} — active-organization extraction. - [`packages/core/app/composables/usePortalFeatures.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/app/composables/usePortalFeatures.ts){rel=""nofollow""} — client contribution collections. - [`packages/core/server/portal.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/server/portal.ts){rel=""nofollow""} — stable session, tenant, authorization, and database adapters. - [`packages/core/server/utils/route-meta.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/server/utils/route-meta.ts){rel=""nofollow""} — route-owned Zod/OpenAPI metadata helper. - [`packages/core/server/utils/openapi-contracts.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/server/utils/openapi-contracts.ts){rel=""nofollow""} — generic contract merging. - [`packages/ui/nuxt.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/ui/nuxt.config.ts){rel=""nofollow""} — neutral UI layer. - [`packages/ui/app/layouts/default.vue`](https://github.com/ludulicious/customer-portal/blob/master/packages/ui/app/layouts/default.vue){rel=""nofollow""} — fallback shell layout. - [`packages/ui/app/pages/dashboard.vue`](https://github.com/ludulicious/customer-portal/blob/master/packages/ui/app/pages/dashboard.vue){rel=""nofollow""} — feature-owned dashboard aggregation. - [`packages/ui/app/components/DashboardContribution.vue`](https://github.com/ludulicious/customer-portal/blob/master/packages/ui/app/components/DashboardContribution.vue){rel=""nofollow""} — component-name resolution and error isolation. ## Platform and feature packages - [`packages/preset/nuxt.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/preset/nuxt.config.ts){rel=""nofollow""} — convenience layer composition. - [`packages/authentication/nuxt.config.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/authentication/nuxt.config.ts){rel=""nofollow""} — authentication pages and configuration. - [`packages/organizations/shared/feature.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/organizations/shared/feature.ts){rel=""nofollow""} — account and organization contributions. - [`packages/administration/shared/feature.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/administration/shared/feature.ts){rel=""nofollow""} — system administration contributions. - [`packages/service-requests/shared/feature.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/service-requests/shared/feature.ts){rel=""nofollow""} — compact business-feature contract. - [`packages/service-requests/server/plugins/openapi-contracts.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/service-requests/server/plugins/openapi-contracts.ts){rel=""nofollow""} — feature-owned API schemas. - [`packages/service-requests/migrations/0000_baseline.sql`](https://github.com/ludulicious/customer-portal/blob/master/packages/service-requests/migrations/0000_baseline.sql){rel=""nofollow""} — clean Service Requests baseline. - [`packages/timesheets/shared/feature.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/timesheets/shared/feature.ts){rel=""nofollow""} — Timesheets modules, policies, widgets, and surface contribution. - [`packages/timesheets/app/components/TimesheetsAdministrationOrganizationPanel.vue`](https://github.com/ludulicious/customer-portal/blob/master/packages/timesheets/app/components/TimesheetsAdministrationOrganizationPanel.vue){rel=""nofollow""} — optional administration surface panel. - [`packages/timesheets/server/db/schema/timesheets.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/timesheets/server/db/schema/timesheets.ts){rel=""nofollow""} — feature-owned PostgreSQL records. - [`packages/timesheets/migrations/0000_baseline.sql`](https://github.com/ludulicious/customer-portal/blob/master/packages/timesheets/migrations/0000_baseline.sql){rel=""nofollow""} — clean Timesheets baseline. ## Kit and verification - [`packages/kit/src/runtime.mjs`](https://github.com/ludulicious/customer-portal/blob/master/packages/kit/src/runtime.mjs){rel=""nofollow""} — config resolution, ordering, locking, journals, and adoption. - [`packages/kit/bin/nuxt-customer-portal.mjs`](https://github.com/ludulicious/customer-portal/blob/master/packages/kit/bin/nuxt-customer-portal.mjs){rel=""nofollow""} — CLI commands. - [`legacy/drizzle/0021_configurable_internal_approvals.sql`](https://github.com/ludulicious/customer-portal/blob/master/legacy/drizzle/0021_configurable_internal_approvals.sql){rel=""nofollow""} — unchanged legacy history example. - [`packages/core/test/layer-boundaries.test.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/test/layer-boundaries.test.ts){rel=""nofollow""} — forbidden aliases, optional imports, locale parity, and brand isolation. - [`packages/core/test/openapi-contracts.test.ts`](https://github.com/ludulicious/customer-portal/blob/master/packages/core/test/openapi-contracts.test.ts){rel=""nofollow""} — route-owned API registry checks. Continue with [Create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer) or consult the [glossary](https://nuxt-customer-portal.com/reference/glossary). # Operations These runbooks cover the work after Customer Portal has been deployed: protecting its data, detecting failures, diagnosing common problems, and recovering safely from an unsuccessful upgrade. ::card-group :::card --- icon: i-lucide-database-backup title: Backup and restore to: https://nuxt-customer-portal.com/operations/backup-and-restore --- Define recovery targets, protect a PostgreSQL backup, and prove that it can be restored. ::: :::card --- icon: i-lucide-wrench title: Troubleshooting to: https://nuxt-customer-portal.com/operations/troubleshooting --- Diagnose startup, database, authentication, email, and layer-discovery failures. ::: :::card --- icon: i-lucide-activity title: Observability to: https://nuxt-customer-portal.com/operations/observability --- Understand the signals available today and the minimum production monitoring baseline. ::: :::card --- icon: i-lucide-shield-alert title: Upgrade recovery to: https://nuxt-customer-portal.com/operations/upgrade-recovery --- Respond to failed migrations, application regressions, and schema compatibility problems. ::: :: ## Own the recovery contract Every deployment should name an operator, define a recovery point objective (how much data may be lost), and define a recovery time objective (how long recovery may take). The deployment owner also needs access to the source revision, database backups, platform secrets, DNS, and email-provider configuration before an incident begins. These pages describe the current repository behavior. Adapt the commands to the database and hosting platform, then rehearse them against non-production infrastructure. # Backup and restore Customer Portal keeps its mutable application state in PostgreSQL. A complete database backup therefore protects accounts, organizations, sessions, feature data, timesheets, invoices, invoice attachments, and recorded email-delivery history. It is also a high-value secret. The database contains password hashes, session and OAuth tokens, personal and financial records, attachment contents, and organization-specific Resend API keys. Encrypt backups at rest, restrict access, record access, and apply a retention policy appropriate to that data. ## Decide the recovery target For production, use the hosting provider's automated backups and point-in-time recovery when available. Choose and document: - the recovery point objective (RPO), which determines backup frequency; - the recovery time objective (RTO), which determines how quickly a restore must be ready; - backup retention and geographic redundancy; - who can start, validate, and approve a restore; - how often the team performs a recovery drill. PostgreSQL's [continuous archiving and point-in-time recovery](https://www.postgresql.org/docs/current/continuous-archiving.html){rel=""nofollow""} can restore to a chosen point between base backups. A periodic logical dump remains useful for portable restore tests and smaller deployments. ## Create a logical backup Run `pg_dump` from a trusted machine with encrypted transport to PostgreSQL: ```bash [Terminal] pg_dump --format=custom --no-owner --no-acl \ --file=customer-portal-YYYY-MM-DD.dump \ "$DATABASE_URL" ``` Use a `pg_dump` client from the same PostgreSQL major version as the server or a newer one that supports it. An older client refuses to dump a newer server, and dump output is not guaranteed to load into an older server. See the official [`pg_dump` compatibility notes](https://www.postgresql.org/docs/current/app-pgdump.html){rel=""nofollow""}. Avoid placing a literal database password in shell history. Supply the connection through the deployment secret manager, a short-lived environment, or PostgreSQL's supported password-file mechanism. After the command succeeds: ```bash [Terminal] pg_restore --list customer-portal-YYYY-MM-DD.dump ``` Store the archive encrypted and separately from the production database. Record the source commit, PostgreSQL version, backup time, file checksum, and operator with it. ::warning A database backup does not include deployment environment variables, OAuth client secrets, the platform Resend key, DNS/provider settings, source code, or the container image. Keep a separate encrypted configuration inventory and an immutable reference to the deployed commit or image. :: ## Restore into an isolated database Never make the first restore attempt over production. Create a new empty database with the expected extensions and privileges, then restore: ```bash [Terminal] pg_restore --dbname="$RESTORE_DATABASE_URL" \ --no-owner --no-acl --exit-on-error \ customer-portal-YYYY-MM-DD.dump ``` `--exit-on-error` prevents a partial failure from being mistaken for success. For a suitably sized archive, `--single-transaction` can make the restore atomic. The official [`pg_restore` reference](https://www.postgresql.org/docs/current/app-pgrestore.html){rel=""nofollow""} explains both options and recommends restoring into a truly empty database. Treat every archive as executable database input and restore only backups you trust. Start the exact Customer Portal commit recorded with the backup. The repository Docker entrypoint automatically applies migrations, so do not point an unvalidated newer image at the restored database. If the objective is an upgrade rehearsal, first prove the original revision and data, take another snapshot, and only then test the target revision. ## Validate the recovery The restore is not complete until an operator verifies it. At minimum: 1. confirm the migration history and application startup succeed; 2. sign in with a test operator and switch between expected organizations; 3. inspect memberships and authorization boundaries; 4. open representative time entries, approvals, invoices, and invoice email history; 5. download an invoice attachment and generate an invoice PDF; 6. exercise a non-destructive database-backed workflow; 7. compare important row counts and timestamps with the backup record. Do not send real email or invoke production OAuth callbacks from the isolated environment. Replace or disable external credentials first. If production secrets were restored into a less-trusted environment, rotate them after the drill. Schedule recurring restore tests. A successful backup job proves that a file was written; only a verified restore proves that the service can recover. # Troubleshooting Start with the first failing boundary: process startup, PostgreSQL, authentication, platform email, organization invoice email, or a feature layer. Capture the deployed commit, timestamp, affected organization and role, request path, and the earliest relevant server error before changing configuration. ## The container exits before serving traffic The repository entrypoint runs `drizzle-kit migrate` before the Nuxt server. Any migration error exits the container deliberately. 1. Read the migration output immediately before `Migration failed!`. 2. Confirm `DATABASE_URL` reaches the intended database from the release environment. 3. Confirm the database user can create and alter the required schemas, tables, indexes, and constraints. 4. Confirm the image and `drizzle/` history come from the same source commit. 5. Compare the recorded migration journal with the expected history. 6. Reproduce against a restored copy before attempting a production repair. Do not delete migration journal rows, drop schemas, or repeatedly restart a destructive migration without understanding its state. Prefer a reviewed forward repair. See [database migrations](https://nuxt-customer-portal.com/architecture/database-migrations) and [upgrade recovery](https://nuxt-customer-portal.com/operations/upgrade-recovery). ## The application cannot use PostgreSQL Check the complete connection path: DNS, network policy, TLS requirements, credentials, database name, connection limits, and database availability. A successful homepage request is not proof of a healthy database-backed session. If failures appear only under load, inspect PostgreSQL connection counts, CPU, memory, storage, locks, and slow queries. If they began after a release, compare the target commit's schema and query changes with the previous pin. ## GitHub or Google sign-in fails Provider buttons are controlled by public feature flags, while the server registers a provider only when both its flag and credentials are present. If credentials are absent, explicitly set the corresponding flag to `false`; otherwise a button can be visible without a working server provider. Verify all of the following: - `BETTER_AUTH_URL` or `PUBLIC_URL` is the externally visible HTTPS origin; - the provider application uses that exact origin and scheme; - GitHub's callback is `/api/auth/callback/github`; - Google's callback is `/api/auth/callback/google`; - the client ID and secret belong to the same provider application; - proxy headers preserve the public host and protocol; - the origin is trusted when a custom proxy or additional frontend is involved. Better Auth documents the callback paths and provider setup for [GitHub](https://better-auth.com/docs/authentication/github){rel=""nofollow""} and [Google](https://better-auth.com/docs/authentication/google){rel=""nofollow""}. For GitHub accounts with a private email address, also verify the OAuth application's email permission as described in the GitHub provider guide. ## OTP, invitation, or recovery email does not arrive Platform authentication email requires both `RESEND_API_KEY` and `RESEND_FROM_EMAIL`. When either prerequisite is missing, the current email utility logs a warning and skips delivery; the initiating UI flow may still appear to continue. 1. Search server logs for `Email prerequisites not met`, `RESEND_API_KEY`, or `RESEND_FROM_EMAIL`. 2. Verify the sender domain and address in Resend. 3. Inspect the provider's delivery, suppression, bounce, and spam information. 4. Check that `PUBLIC_URL` and `BETTER_AUTH_URL` produce reachable links. 5. Retest with a controlled recipient and correlate the timestamp with server and provider logs. Do not log or paste OTPs, reset links, session cookies, or API keys into a public issue. ## Invoice email cannot be configured or sent Invoice email uses an organization-specific Resend credential configured by an organization owner under `/settings/organization`; it does not use the platform authentication email key. - The API key must be allowed to list Resend domains. A send-only restricted key is rejected because Customer Portal validates domain ownership and sending status. - The selected sender domain must be verified and enabled for sending. Use **Check again** after changing provider state; ordinary results are cached briefly. - The invoice sender address must use a verified domain. - A single attachment may be at most 10 MB, and all email attachments together may be at most 40 MB. - Inspect the invoice's email history. Customer Portal records `PENDING`, `SENT`, or `FAILED`, the provider message ID, and the latest checked provider status. Use the invoice action to refresh delivery status when Resend has accepted a message but final delivery is unclear. See [create and manage invoices](https://nuxt-customer-portal.com/guides/invoicing) for the user workflow. ## A feature layer does not appear Customer Portal discovers directories immediately below `layers/` that contain `nuxt.config.ts`. 1. Confirm the directory is not nested one level too deep. 2. Confirm its `nuxt.config.ts` loads without an import error. 3. Restart the development server after adding, removing, or renaming the layer. 4. Check the feature definition, audience and policy registration, navigation placement, and required locale keys. 5. Confirm its schema files match the root Drizzle glob and its migrations are present. 6. Test with the correct active organization and role; authorized navigation can be intentionally hidden. Continue with [feature layers](https://nuxt-customer-portal.com/architecture/layers) or the [feature contract reference](https://nuxt-customer-portal.com/reference/feature-contract). # Observability Customer Portal currently writes application and migration messages to standard output and error. PostgreSQL and Resend provide additional service-level signals, while invoice email history records provider message state in the database. There is not yet a dedicated health endpoint, structured logging contract, application metrics, distributed tracing, or bundled error-tracking integration. Treat those as contribution opportunities, not as existing capabilities. ## Minimum monitoring baseline | Signal | Alert when | Why it matters | | -------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Process and restarts | the service is unavailable or repeatedly restarts | migration or startup failure can prevent Nuxt from serving | | HTTPS probe | `/` fails, times out, or has abnormal latency | confirms public routing and the Node process | | PostgreSQL | connections, CPU, memory, storage, locks, or query latency cross safe limits | most authenticated and feature workflows depend on the database | | HTTP errors | 5xx rate or latency changes materially | catches application regressions that a homepage probe misses | | Migration output | the release job or Docker entrypoint exits non-zero | new code must not run against an unexpected schema | | Authentication email | send errors or prerequisite warnings appear | verification, invitations, OTP, and recovery can stop working silently for users | | Invoice delivery | failed or persistently pending messages increase | invoices can be generated successfully but not delivered | | Backups | the newest verified backup exceeds the RPO | availability without recoverability is incomplete | | Restore drills | no successful drill exists within the agreed interval | a stored archive may still be unusable | An HTTP probe of `/` confirms only the public web path. It does not prove PostgreSQL, authentication, authorization, OAuth, or email delivery are healthy. ## Release smoke checks After every production release, run a small set of controlled checks: 1. load the homepage and sign-in page over the public HTTPS origin; 2. authenticate with a non-privileged test account; 3. select its expected organization and load a protected page; 4. read a representative, database-backed feature view; 5. exercise any workflow changed by the release; 6. inspect startup, migration, server, database, and provider signals. Keep routine smoke checks non-destructive. Do not create invoices, send customer email, approve time, or mutate financial records unless the deployment has dedicated synthetic data and cleanup rules. ## Handle logs as sensitive data Current log messages can include request paths, email addresses, subjects, organization context, and provider errors. Restrict access and retention. Never log cookies, authorization headers, OAuth tokens, password-reset links, OTPs, API keys, database URLs, attachment bodies, or complete request payloads. When sharing an incident excerpt, redact personal data and secrets while preserving the timestamp, source revision, route pattern, error type, and correlation information needed for diagnosis. ## Recommended instrumentation work A production-focused contribution should introduce these capabilities as explicit, reviewed contracts: - structured server logs with request or correlation IDs; - redaction at the logging boundary; - a process-only liveness endpoint and a database-aware readiness endpoint; - request rate, latency, error, job, email, and migration metrics; - error tracking with source revision and deployment environment; - documented retention, access, privacy, and alert ownership. Keep liveness independent of PostgreSQL so an orchestrator does not restart healthy processes during a database outage. Readiness may fail when the application cannot safely serve database-backed traffic. Avoid returning configuration, dependency versions, database details, or secrets from either endpoint. Propose this work as a reusable module or core capability in [module proposals](https://nuxt-customer-portal.com/contributing/propose-a-module). # Upgrade recovery Customer Portal currently ships from pinned source commits rather than versioned releases. Every upgrade can change application code, dependencies, environment expectations, and the shared root migration history. Record the old and target commits before release and keep a tested database recovery point. ## Identify the failure class | Failure | Typical evidence | First response | | --------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Migration fails before startup | the entrypoint exits after `Migration failed!` | stop retrying, preserve logs, inspect database state on a restored copy | | Migration succeeds but Nuxt fails | migration completed, process then exits or never becomes ready | preserve the migrated database, compare runtime config and target build | | Application regression | service starts but a workflow, permission, or integration fails | stop rollout, limit traffic, determine whether the schema remains compatible with the old application | | Data or authorization defect | incorrect records or cross-tenant exposure are possible | restrict access, preserve evidence, invoke the security process when confidentiality may be affected | Do not assume redeploying the old image is a complete rollback. An applied migration remains applied, and old code may not understand the new schema. ## Contain and diagnose 1. Stop additional replicas and automated retries from changing the same database. 2. Record timestamps, source and image identifiers, migration output, and the last known healthy revision. 3. Take a new database snapshot before attempting repair, even if the state is partially migrated. 4. Determine which migration statements committed and whether the application served traffic afterward. 5. Reproduce with a copy of the pre-upgrade backup and the target commit. 6. Decide among application rollback, a forward repair, or database restore. Never edit the migration journal merely to make a migration appear complete. Never drop a feature schema as a generic rollback: removing a layer intentionally leaves its production data in place. ## Choose a recovery path ### Roll back application code Use the previous image only when the new schema is demonstrably backward-compatible. Expand-and-contract migrations are designed to make this possible: old structures remain available while the new application begins using additive structures. Run the old revision against a restored copy of the current database first. If it reads or writes removed columns, changed constraints, or transformed data incorrectly, use another path. ### Repair forward Prefer a reviewed forward migration when production has already accepted writes on the new schema or a restore would exceed the recovery objective. The repair should be generated and committed with the application change, rehearsed on a copy, and applied once through the normal release mechanism. ### Restore the database Restore the last verified recovery point into a new empty database, start the exact application revision recorded with that backup, and complete the [restore validation checklist](https://nuxt-customer-portal.com/operations/backup-and-restore#validate-the-recovery). Keep the failed database isolated for diagnosis. Cut traffic over only after application, data, and authorization checks pass. A restore loses writes after its recovery point. The incident owner must compare that loss with the agreed RPO and coordinate any reconciliation before reopening writes. ## Recover configuration separately Database recovery does not restore hosting configuration. Compare environment variables, public URLs, OAuth callbacks, proxy settings, DNS, and email-provider credentials with the last known healthy inventory. Roll back configuration changes independently and rotate any secret whose handling became uncertain. ## Close the incident After service is stable: - document the trigger, impact, timeline, and chosen recovery path; - reconcile writes or external emails around the recovery window; - add a test that would have caught the failure; - improve the migration, release gate, monitor, or runbook; - perform a fresh backup and schedule another recovery drill. The preventive workflow is documented in [compatibility and releases](https://nuxt-customer-portal.com/reference/compatibility-and-releases) and [deployment](https://nuxt-customer-portal.com/getting-started/deployment). # User guides These guides describe the product people use, not only the code contributors extend. They follow the current Customer Portal routes, permissions, and workflow states. ## Choose your role ::card-group :::card --- icon: i-lucide-user-round title: Portal member to: https://nuxt-customer-portal.com/guides/account-and-organizations --- Sign in, accept an invitation, switch organizations, and manage your account. ::: :::card --- icon: i-lucide-layout-dashboard title: Authenticated portal user to: https://nuxt-customer-portal.com/guides/dashboard --- Read role-aware dashboard cards and continue work in the active organization. ::: :::card --- icon: i-lucide-ticket-check title: Service-request participant to: https://nuxt-customer-portal.com/guides/service-requests --- Create and follow organization service requests or manage the shared queue. ::: :::card --- icon: i-lucide-clock-3 title: Time registrant to: https://nuxt-customer-portal.com/guides/time-entry --- Record a week manually or with a timer, then submit it for review. ::: :::card --- icon: i-lucide-stamp title: Approver or client reviewer to: https://nuxt-customer-portal.com/guides/approvals --- Review submitted time, request changes, and configure reviewer access. ::: :::card --- icon: i-lucide-settings-2 title: Organization administrator to: https://nuxt-customer-portal.com/guides/timesheet-setup --- Configure clients, activities, projects, rates, approvals, and invoice defaults. ::: :::card --- icon: i-lucide-chart-no-axes-combined title: Reporting administrator to: https://nuxt-customer-portal.com/guides/timesheet-reporting --- Filter recorded time, interpret totals, and export operational data. ::: :::card --- icon: i-lucide-receipt-text title: Invoice administrator to: https://nuxt-customer-portal.com/guides/invoicing --- Create invoices from approved time, deliver them, and track payment. ::: :::card --- icon: i-lucide-shield-check title: System administrator to: https://nuxt-customer-portal.com/guides/system-administration --- Manage installation-wide users, organizations, sessions, and feature access. ::: :: ## How permissions affect the interface Customer Portal combines an active organization with the user’s role and feature-specific capabilities. A route may exist without appearing in a person’s menu when that person has no relevant work or permission. - Organization owners and administrators configure the workspace and receive every timesheets action. - Members can read, create, update, and submit time when time registration is enabled for them. - Internal approvers see weeks assigned to them. - Client reviewers see only supplier workspaces and submitted time for which they have access. - System administrators receive the platform’s standard administrative bypass. System administration is installation-wide and especially sensitive. Use [Administer the portal](https://nuxt-customer-portal.com/guides/system-administration) for those workflows rather than treating an organization administrator as a system administrator. If a guide mentions a control you cannot see, first confirm the active organization, your organization role, and the relevant workspace capability. # Accounts and organizations Customer Portal users have one account and can belong to multiple organizations. Most authenticated work happens inside one **active organization**, which determines the data and feature permissions for each request. ## Create or access an account The configured registration mode determines the available path: | Mode | What a visitor can do | | ----------------- | ------------------------------------------------------------------ | | `open` | Create an account from `/signup` | | `invitation-only` | Register only through an organization invitation | | `disabled` | Sign in to an existing account; public registration is unavailable | Email/password registration requires a name, email address, and password of at least eight characters. After registration, `/verify-email` asks for the six-digit one-time code sent to the email address, then returns you to `/login`. A password login for an unverified address sends a new sign-in code and returns to the intended route after verification. GitHub and Google buttons appear on `/login` only when their providers are enabled by the deployment. A successful sign-in normally opens `/dashboard` or the protected route that originally sent you to the login page. Use `/forgot-password` to request a reset code and choose a new password. The reset form requires the code and matching passwords. ## Accept an invitation Open the invitation link using the email address to which it was sent. - If you need an account, registration preserves the invitation while email verification is completed. Sign in afterward to accept it automatically. - If you are already signed in with the invited address, Customer Portal accepts the invitation and opens the dashboard. - If the signed-in email does not match the invitation, sign out and use the invited address. An invitation grants an organization role; it does not grant system-administrator access. ## Switch the active organization Open **My organizations** at `/my-organizations`. Each membership shows its role and whether it is active. Choose **Set active** on another organization before opening its dashboard or feature pages. ::warning Always check the active organization before creating, approving, exporting, or deleting tenant-owned information. Server routes scope their work to that active context. :: ## Understand roles | Role | Scope | Typical responsibility | | -------------------- | ---------------- | ----------------------------------------------------- | | Owner | One organization | Full organization control and membership governance | | Admin | One organization | Organization configuration and feature administration | | Member | One organization | Everyday work allowed by each feature policy | | System administrator | Entire portal | Manage portal users and organizations | System administrators are configured separately through `ADMIN_EMAILS`. Organization owners and administrators do not automatically become system administrators. If you hold that installation-wide role, use [Administer the portal](https://nuxt-customer-portal.com/guides/system-administration) for user, session, and cross-organization operations. ## Manage profile and organization Open `/settings` to update your name, email address, and profile image. Use `/settings/security` to change the account password. The `/settings/notifications` screen is currently a non-persistent interface preview. Its switches do not save notification preferences or establish a delivery guarantee; do not rely on them for operational alerts. At `/settings/organization`, authorized owners and administrators can review organization details, members, and invitations. The controls shown there depend on the active organization and Better Auth organization permissions. Continue with [the dashboard](https://nuxt-customer-portal.com/guides/dashboard) to understand the work shown for your role, [timesheet workspace setup](https://nuxt-customer-portal.com/guides/timesheet-setup) if you administer the production module, or [record a week](https://nuxt-customer-portal.com/guides/time-entry) if the workspace is ready. # Use the dashboard The dashboard at `/dashboard` is the first authenticated overview of Customer Portal. Portal core supplies the layout; installed feature layers contribute the actual widgets. What you see therefore depends on the active organization, its enabled capabilities, and your role in the work. ## Check the organization context Before acting on a dashboard item, confirm the active organization in the portal header. Switching organizations changes the capabilities and tenant-owned records available to the dashboard. A widget may disappear after a switch when the destination organization does not enable that feature or your membership does not carry the required capability. This is expected role-aware behavior, not a missing global record. ## Read the dashboard areas Portal core orders contributions into three areas: | Area | Purpose | | --------- | ------------------------------------------------------ | | Attention | Reviews, approvals, and other work waiting for action | | Main | Current operational work and frequently used summaries | | Aside | Supporting context and compact overviews | Features choose an area, relative order, and full, half, or third width for each contribution. Portal core combines them into one responsive page without requiring feature-specific imports in the dashboard itself. ## Timesheets and invoice widgets The production timesheets layer contributes only the cards your current capabilities permit: - **My week** for people who can enter time; - **Internal approvals** for assigned internal approvers; - **Client approvals** for client reviewers; - **Supplier timesheets** for people allowed to view supplier work; - **Sales invoices** for invoice administrators; - **Received invoices** for organizations with client invoice access. Open a card to continue in the corresponding workflow. Counts and visibility refresh when the active organization changes. Use [Record and submit time](https://nuxt-customer-portal.com/guides/time-entry), [Approve timesheets](https://nuxt-customer-portal.com/guides/approvals), or [Create and manage invoices](https://nuxt-customer-portal.com/guides/invoicing) for the complete task flow. ## Service-request widgets The service-request layer contributes an overview of active, resolved, and recently updated requests. Organization owners, organization administrators, and system administrators also receive an attention card for requests that need management. Use **New request** from the overview or open a recent item directly. Continue with [Use service requests](https://nuxt-customer-portal.com/guides/service-requests) for status changes, search, and administrative actions. ## Missing or failed widgets When no installed feature contributes a widget for your current context, the dashboard shows an empty state. Check the active organization and ask an organization administrator whether the expected workspace capability is enabled. Each contribution is isolated by an error boundary. If one card fails, the rest of the dashboard remains available and the failed card offers **Retry**. A repeated failure usually points to that feature's API, permissions, or organization configuration; capture the affected organization, role, and request before following [Troubleshooting](https://nuxt-customer-portal.com/operations/troubleshooting). # Administer the portal System administration is installation-wide. It is separate from being an owner or administrator inside one organization and is granted through the deployment's `ADMIN_EMAILS` configuration. Open **Admin → Organizations** at `/admin/organizations` or **Admin → Users** at `/admin/users`. Both the navigation and server routes require the system-administrator role. ::warning System administrators can alter identities, sessions, organization access, and production feature availability. Use named administrator accounts, keep `ADMIN_EMAILS` small, and record the reason for sensitive actions in your operational process. :: ## Manage organizations The organization list can be searched by name or slug and sorted by name or creation date. Select an organization to inspect its details, members, pending invitations, and feature capabilities. To create an organization: 1. open `/admin/organizations/create`; 2. enter a display name; 3. review the generated slug; 4. ensure the slug contains only lowercase letters, numbers, and hyphens; 5. create the organization, then configure its membership and features. An organization slug is used in routes and lookups. Change it deliberately and verify links or integrations that may retain the previous value. On an organization detail page, a system administrator can: - edit its name, slug, and logo; - enable or disable the Timesheets workspace; - enable invoicing when the Timesheets workspace is enabled; - inspect client relationships and their access mode; - configure organization invoice email when invoicing is enabled; - manage members and pending invitations. Disabling the Timesheets workspace also disables invoicing. It does not delete the organization's existing feature data. ## Link or remove members Use **Link existing user** when an account already exists and needs an organization membership. Select the user and an `owner`, `admin`, or `member` organization role. The portal rejects duplicate membership. Removing a membership does not delete the user account. The current administrator cannot remove their own membership through this action, and the last owner of an organization cannot be removed. Use an invitation when the person does not yet have the intended membership. Invitations can be assigned an organization role, resent, or cancelled. A newly created invitation currently expires after two days and requires platform email delivery to be configured. ## Manage users The user list shows name, email, system role, ban state, verification state, and creation date. Search by name or email, then use the row actions for the selected account. Available actions include: - change the system role between `user` and `admin`; - update the user's name or profile image; - set a new password of at least eight characters; - inspect active and expired sessions and revoke another session; - ban a non-administrator, optionally with a reason and expiry; - unban an account; - impersonate a user for diagnosis. The portal prevents changing your own system role, banning an administrator, and revoking your current session from the session manager. ### Use impersonation safely Impersonation replaces the effective browser session and redirects to the dashboard. Every subsequent action is performed as that user until impersonation is stopped. Before starting, record the support reason and obtain appropriate authorization. Avoid changing data unless the support case requires it, never ask the user for their password, and stop impersonation as soon as the check is complete. ::note Customer Portal does not currently include a general administrator audit-log interface. Preserve external operational records for role changes, bans, password resets, impersonation, session revocation, and organization capability changes. :: For delivery failures, use [troubleshooting](https://nuxt-customer-portal.com/operations/troubleshooting). For organization-level setup, continue with [accounts and organizations](https://nuxt-customer-portal.com/guides/account-and-organizations) or [set up a timesheet workspace](https://nuxt-customer-portal.com/guides/timesheet-setup). # Use service requests Service Requests is the compact reference module shipped with Customer Portal. It is a working organization-scoped workflow as well as an implementation example for contributors. ## Understand request visibility Open **My requests** at `/requests`. Despite that navigation label, the current list endpoint is scoped to the entire active organization, not only to requests created by the signed-in user. Members of the same active organization can list and read those records according to the feature policy. ::warning Do not place information in a service request that other members of the active organization must not see. Always verify the active organization before creating or updating a request. :: The current role policy is: | Organization role | Actions | | -------------------- | ---------------------------------------------- | | Owner or admin | Create, read, update, delete, list, and manage | | Member | Create, read, update, and list | | System administrator | Standard platform administrator bypass | `manage` controls the organization management view and access to internal notes. `delete` is not granted to ordinary members. ## Create a request Choose **New request** or open `/requests/new`, then provide: - a title between 3 and 200 characters; - a description between 10 and 5,000 characters; - a priority: **Low**, **Medium**, **High**, or **Urgent**; - an optional category of up to 100 characters. New requests start as **Open** and default to **Medium** priority when no other priority is selected. ## Find and follow requests The list supports text search, status, priority, and category filters. It can be sorted by creation time, status, or priority, and the filter state is retained in the URL so a view can be bookmarked or shared with another authorized member. The four statuses are: | Status | Meaning | | ----------- | -------------------------------- | | Open | Received and not yet in progress | | In progress | Actively being handled | | Resolved | A resolution has been reached | | Closed | Work and follow-up are complete | Select a request to inspect its description, category, priority, status, creation date, and resolution date when available. Updates refresh the record's modification time. ## Manage the organization queue Organization owners and administrators open `/admin/requests`. The management dashboard aggregates request counts and supports queue filtering. Select a request to change its status or priority and maintain internal notes. Internal notes are returned only to users with `manage` access. Moving a request to **Resolved** or **Closed** records the corresponding timestamp. The shared data contract includes assignment to a portal user, but the current administration page does not yet populate the assignee selector. Treat interactive assignment as unavailable until that control is completed. ## Use the dashboard signals The portal dashboard shows active and resolved totals plus recently changed requests. Users with management access also receive an attention view that highlights: - active urgent requests; - active requests without an assignee; - active requests that have remained open for more than seven days. The module's architecture and policy implementation are documented in [Service Requests for contributors](https://nuxt-customer-portal.com/modules/service-requests). Feature authors can use it with [Create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer). # Set up a timesheet workspace Organization owners and administrators configure Timesheets. Until the required structure is complete, members see a waiting message instead of an entry form and administrators see a setup checklist. ## Complete the required checklist Follow the checklist in this order: 1. **Link a client** at `/admin/timesheets/clients`. 2. **Create an active activity** at `/admin/timesheets/activities`. 3. **Configure an active project** at `/admin/timesheets/projects`. 4. **Add default team rates** at `/admin/timesheets/rates`. ### 1. Add a client A client is another portal organization connected to the current workspace. Link an organization you can access, or create a new client organization from the client form. For invoicing, open the client card and add its full address, invoice email, preferred language, and at least one contact. Registration and VAT numbers are optional client metadata. The card also controls whether the client can view or review timesheets and whether its members may receive invoices. ### 2. Add activities Activities describe the kind of work a person records. Mark each one billable or non-billable and keep it active while it should be selectable. An activity with existing time entries cannot be deleted. Make it inactive when historical records must remain but new selection should stop. ### 3. Add projects Every project belongs to a client and exposes one or more activities. A project can also have a code, time or money budget, and person-specific rate overrides. Project-specific rates override a team member’s default rate. Deletion is available only while the project has no time entries; otherwise preserve it for history. ### 4. Configure the team At **Team rates**, decide which members may register time and set their default hourly rates. Billable activities are unavailable to a member when neither a default rate nor a project override exists. A running timer must be stopped before time registration can be disabled for that member. ## Configure workspace and invoice defaults At `/admin/timesheets/settings`, set the currency, timezone, default VAT rate, and sender invoice details. Invoice creation requires the sender address, registration number, VAT number, IBAN, BIC, and invoice email. These organization details are distinct from each client’s recipient address and contact. ## Decide how approval works Open `/admin/timesheets/internal-approvals` to enable or disable internal approval. - When internal approvals are disabled, submitted weeks are approved automatically. - When enabled, choose which members require approval and assign one or more approvers to each of them. - Any assigned approver may complete the review. Client review is configured per client through its timesheet access mode: **Disabled**, **View**, or **Review**. Review access should also have reviewers assigned from `/timesheets/approvals/reviewers`. The workspace is ready when members can follow [time entry](https://nuxt-customer-portal.com/guides/time-entry), and reviewers can follow the [approval workflow](https://nuxt-customer-portal.com/guides/approvals). # Record and submit time Open **Timesheets** at `/timesheets`. The page shows one ISO week at a time and adapts from a weekly grid on large screens to a day-by-day list on smaller screens. ## Before you begin You can enter time when: - the active organization has a client, active activity, configured project, and required team rates; - an administrator has enabled time registration for your membership; - the selected billable activity has either your default hourly rate or a project-specific override; - the week is in **Draft** or **Needs changes** state. ## Add an entry 1. Select the week with the previous and next controls. 2. Choose an empty day or project/activity cell, or select **Add entry**. 3. Select the project and an activity made available by that project. 4. Enter a date, hours and minutes, and an optional note. 5. Save the entry. Choose a populated cell to inspect its entries. Entries can be edited or deleted while the week remains editable. A single day and project/activity combination may contain multiple separate entries. ## Use the timer Choose **Start timer**, select a project and activity, and optionally add a note. The running timer remains visible with its live duration until you choose **Stop**. Only one entry is treated as the running timer. Stop it before submitting the week or before an administrator disables time registration for your membership. ## Submit the week The **Submit** button is available for an editable week that contains time and has no running timer. Submission locks ordinary entry editing and moves the week into the configured approval process. | Status | Meaning | | ------------- | ----------------------------------------------------------------- | | Draft | Entries can be added, changed, or removed | | Submitted | Waiting for the configured internal review | | Approved | Internal review is complete, or approval was automatic | | Needs changes | An approver rejected the week with a reason; edit and resubmit it | When a week needs changes, the rejection reason appears above the entries. Correct the relevant records and submit the week again. Client review, when enabled for a linked organization, happens after supplier-side approval and is tracked separately for that client’s slice of the time. See [approve timesheets](https://nuxt-customer-portal.com/guides/approvals). # Approve timesheets The Timesheets layer supports two review boundaries: an organization can approve its own members’ weeks, and a linked client can review the approved time relevant to that client. ## Internal approval Organization owners and administrators configure internal approval at `/admin/timesheets/internal-approvals`. Enable the workflow, mark which members require review, and assign their approvers. Assigned approvers open `/timesheets/internal-approvals`. Each item shows the person, week, total time, billable value, daily totals, entries, rates, notes, and any client-review status. For a submitted week: - **Approve** completes internal review. - **Reject** requires a reason and returns the week to the member as **Needs changes**. - **Reopen** unlocks a previously approved week for correction. When internal approval is disabled, submitted weeks are approved automatically. When it is enabled for a member, at least one assigned approver should be available to avoid a stranded queue. ## Client access modes On the supplier workspace’s client record, choose one of: | Mode | Client capability | | -------- | --------------------------------------------------------------------------- | | Disabled | No client timesheet access | | View | Authorized client members can inspect supplier timesheets and billing state | | Review | Assigned reviewers can approve or dispute submitted client time | Organization owners and administrators on the client side assign reviewers at `/timesheets/approvals/reviewers`. Assignment is per supplier workspace and updates immediately. ## Client review An assigned reviewer opens `/timesheets/approvals` and selects a supplier timesheet. The detail shows the supplier, person, week, entries, total hours, status, and event history. - **Approve** accepts the client slice. - **Dispute** requires a comment explaining what needs attention. The reviewer list can be filtered by supplier and status and sorted by week, supplier, person, hours, or status. If review data changes while the page is open, the conflict is reported and the list refreshes before another decision is made. Client users with view access use `/timesheets/suppliers` to inspect supplier time and whether it is awaiting, partially included in, or fully included in invoices. Approved, uninvoiced billable entries can then feed the [invoice workflow](https://nuxt-customer-portal.com/guides/invoicing). # Report on recorded time Organization owners, organization administrators, and system administrators can open `/admin/timesheets/reports`. The report reads time entries from the active organization and uses the values captured when each entry was recorded. ## Run a report The current page exposes four optional filters: - **From** — include entries on or after this date; - **To** — include entries on or before this date; - **Client** — include projects for one client organization; - **Project** — include entries for one project. Choose **Run report** to calculate the result. The summary shows total hours, billable hours, non-billable hours, and billable amount. The detail table shows date, client, project, person, activity, hours, and amount for every matching entry. ::warning The page does not currently expose a status filter. Its default result can therefore contain entries from draft, submitted, approved, and rejected weeks. Check the exported status column or use the API's status filter before treating a result as approved billing data. :: ## Interpret money and time Report rows use the duration, billable flag, hourly rate, and currency snapshot stored with each time entry. Changing a project rate later does not recalculate historical rows. Amounts are derived from recorded minutes and the snapshotted hourly rate. The report uses the active workspace currency for its displayed summary. If historical entries contain another currency, inspect the CSV's per-row currency before aggregating them outside Customer Portal. ## Export CSV Choose **Export CSV** to download `timesheets.csv` using the filters currently entered on the page. You do not need to run the on-screen report first. The export contains: - date, client, project, person, and activity; - decimal hours and billable state; - hourly rate, amount, and currency; - weekly timesheet status; - the entry note. Spreadsheet software can infer dates, numbers, or formulas from CSV cells. Treat the file as untrusted input when opening data supplied by users, restrict access because it contains personal and financial information, and store exports only as long as needed. ## Use additional API filters The report endpoint also accepts user, activity, billable-state, and weekly-status filters even though those controls are not yet exposed by the page. Inspect `/api-docs` on the deployed portal for the exact current query contract under `GET /api/timesheets/admin/report`. API access uses the current authenticated session, active organization, and Timesheets `report` permission. The generated interface is described in [Server API and OpenAPI](https://nuxt-customer-portal.com/reference/server-api). Continue with [approve timesheets](https://nuxt-customer-portal.com/guides/approvals) when the report exposes unfinished weeks, or [create and manage invoices](https://nuxt-customer-portal.com/guides/invoicing) after billable work is approved. # Create and manage invoices Organization owners and administrators manage sales invoices at `/admin/timesheets/invoices`. Linked client organizations receive issued invoices through `/timesheets/invoices` when invoice access is enabled. ## Prepare invoice data Before creating an invoice: 1. Complete sender details under `/admin/timesheets/settings`. 2. Complete the client address and invoice email under `/admin/timesheets/clients`. 3. Add at least one client contact. 4. Configure currency and default VAT. 5. For time-based billing, approve the relevant billable time and ensure its rates are correct. The sender form requires address, registration number, VAT number, IBAN, BIC, and invoice email. The creation wizard redirects there when required details are missing. ## Create a draft Choose **New invoice**, then select a source. ### Free-form invoice Select a client and compose the invoice lines manually. Each line has a description, quantity, unit price, and VAT rate. ### Approved timesheets 1. Select the billing period. 2. Select a client with approved, uninvoiced billable entries. 3. Select one or more projects. 4. Choose detailed lines or a summary grouped by project, person, activity, or person and activity. 5. Review the generated quantities, rates, VAT, subject, notes, issue date, due date, and number. Creating the invoice saves it as **Draft**. Time entries attached to those lines are no longer offered as uninvoiced work for another draft. ## Issue and deliver Open the draft and choose **Issue and send**. Review the recipient, language, subject, message, CC addresses, and attachments before delivery. Customer Portal generates the PDF and records the delivery in invoice history. Issued invoices can be sent again. Overdue invoices expose a separate payment-reminder action. Delivery tracking distinguishes Customer Portal’s send record from provider events such as accepted, delivered, delayed, bounced, failed, suppressed, complained, opened, or clicked. An open event is approximate and does not prove a person read the invoice. ## Record payment and status | Status | Meaning | | ----------- | ---------------------------------------------------------------- | | Draft | Editable invoice that has not been issued | | Issued | Sent or otherwise issued with an outstanding balance | | Paid | Registered payments cover the invoice balance | | Written off | The balance is no longer treated as collectible; history remains | Use **Register payment** on an issued invoice. Enter the payment date, amount, and optional reference. Partial payments reduce the outstanding amount; the invoice becomes **Paid** when its balance is covered. Writing off an invoice preserves it and its history. **Undo write-off** restores its previous draft or issued state. ## Attachments, PDFs, and client access Files added to an invoice can be included in future emails. The email flow enforces a combined attachment limit of 40 MB. Both administrators and authorized client viewers can print or download the generated invoice PDF; client viewers can also download shared attachments. Enable invoice access on the supplier’s client record. Client organization owners and administrators always have access and can assign additional viewers at `/timesheets/invoices/viewers`. # Contributing Customer Portal is intended to grow through collaboration. Contributions do not need to begin with a large module: a clearer guide, a failing test, a reproducible issue, or a focused accessibility improvement can be the most useful first change. ## Ways to contribute - Report a reproducible bug or confusing workflow. - Improve documentation, examples, diagrams, and terminology. - Add tests around authorization, tenancy, or feature boundaries. - Improve accessibility, responsive behavior, or translations. - Fix or extend an existing layer. - Propose a reusable business capability as a new feature layer. ## Before opening code 1. Search existing [issues](https://github.com/ludulicious/customer-portal/issues){rel=""nofollow""} and pull requests, then choose the matching [structured issue form](https://github.com/ludulicious/customer-portal/issues/new/choose){rel=""nofollow""}. 2. Describe the user problem and the affected layer. 3. For a substantial feature, discuss the boundary before building it. A good layer owns one coherent business capability. 4. Keep the first pull request reviewable; separate infrastructure changes from product behavior when possible. ## Local checks Run the checks that match your change and, before requesting review, run the full project suite: ```bash [Terminal] pnpm test:features pnpm lint pnpm typecheck pnpm build ``` Database changes also require a generated migration that has been reviewed and applied to a disposable PostgreSQL database. ## Review principles Contributions are easier to merge when they: - preserve the headless core boundary; - scope every tenant-owned query to the active organization; - include English and Dutch keys together; - use explicit request, persistence, and response types; - document behavior and tradeoffs, not only implementation; - avoid coupling the host application to a feature. Have an idea for a reusable capability? Read [module proposal guidance](https://nuxt-customer-portal.com/contributing/propose-a-module), then open the [reusable layer proposal form](https://github.com/ludulicious/customer-portal/issues/new?template=module-proposal.yml){rel=""nofollow""}. When the boundary is agreed, continue with [create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer), [distribute the layer](https://nuxt-customer-portal.com/contributing/distribute-a-layer), and [testing contributions](https://nuxt-customer-portal.com/contributing/testing). Documentation-only changes have a separate [writing guide](https://nuxt-customer-portal.com/contributing/documentation). Use [Community and support](https://nuxt-customer-portal.com/contributing/community) to choose the right channel and report security concerns privately. # Create a feature layer A local feature is an ordinary Nuxt layer plus a `PortalLayerManifest`. Keep it self-contained and access the platform only through documented package exports. ## Register the provider ```ts [portal.config.ts] import { definePortalConfig, localPortalLayer } from '@nuxt-customer-portal/kit' export default definePortalConfig({ layers: [ '@nuxt-customer-portal/preset', localPortalLayer({ id: 'notes', source: './layers/notes', schema: './layers/notes/server/db/schema', migrations: './layers/notes/migrations', dependsOn: ['core'] }) ] }) ``` The source needs `nuxt.config.ts` with a stable `$meta.name`. Resolve local CSS or assets from `import.meta.url`, not the host root. ## Define the feature ```ts [layers/notes/shared/feature.ts] import type { PortalFeatureDefinition } from '@nuxt-customer-portal/core/feature' export type NoteAction = 'view' | 'manage' export const notesFeature: PortalFeatureDefinition = { id: 'notes', modules: [{ id: 'notes', labelKey: 'notes.title', to: '/notes', routePrefixes: ['/notes'], audiences: ['authenticated'] }], dashboardWidgets: [{ id: 'notes-overview', component: 'NotesDashboardWidget', area: 'aside', size: 'full' }], policy: { owner: ['view', 'manage'], admin: ['view', 'manage'], member: ['view'] } } ``` Register it from a client plugin with `usePortalFeatures().register(notesFeature)`. Component names are serializable strings resolved by the UI package; contracts do not depend on Nuxt UI or Vue component types. Because Nuxt cannot discover components that appear only as runtime strings, register the layer's components globally. Nuxt keeps global components lazy, so they remain split from the main application bundle: ```ts [nuxt.config.ts] import { fileURLToPath } from 'node:url' export default defineNuxtConfig({ components: [{ path: fileURLToPath(new URL('./app/components', import.meta.url)), global: true }] }) ``` Use a surface contribution when another screen should host an optional panel: ```ts surfaces: [{ id: 'notes-organization-panel', surface: 'administration.organization.detail', component: 'NotesOrganizationPanel', order: 40 }] ``` Administration discovers the contribution without importing your package. ## Author tenant-safe routes ```ts [layers/notes/server/api/notes/index.get.ts] import { requireFeatureAccess } from '@nuxt-customer-portal/core/server' import { definePortalRouteMeta } from '@nuxt-customer-portal/core/route-meta' import { notesFeature } from '../../../shared/feature' defineRouteMeta(definePortalRouteMeta({ operationId: 'notesListGet', query: notesListQuerySchema })) export default defineEventHandler(async (event) => { const context = await requireFeatureAccess(event, notesFeature.policy, 'view') return listNotes(context.organizationId) }) ``` The route-owning package owns its Zod validation and OpenAPI metadata. Every query must derive tenant scope from the authenticated context; never trust an organization ID supplied by a caller. ## Own schema and migrations Use a package-owned PostgreSQL schema such as `pgSchema('notes')`. Extend official data with foreign keys from host-owned tables; do not add columns to official tables unless you take over that provider's migration stream. Generate a local migration with: ```bash npx nuxt-customer-portal db generate --provider notes ``` Commit schema and migration together. Test locale parity, policy behavior, unauthorized and cross-organization requests, fresh migration, repeat migration, failure rollback, and a build both with and without the local layer. # Distribute a feature layer Official layers use Nuxt's npm-layer model: the package entry point is `nuxt.config.ts`, consumers compose it with `extends`, and the layer declares a stable `$meta.name`. ## Package contract A distributable layer should provide: - `name`, SemVer `version`, `publishConfig.access`, explicit `files`, and explicit `exports`; - Nuxt and shared framework libraries as compatible peers and development dependencies; - every library imported at runtime in its owning package's dependencies; - a `portal-manifest` export with provider ID, version, dependencies, schema, and immutable migrations; - public `feature`, `types`, and `schema` exports when applicable; - no host aliases, repository-relative imports, demo branding, or physical cross-package paths. Package code imports core services from `@nuxt-customer-portal/core/server` and contracts from `@nuxt-customer-portal/core/feature`. Cross-organization requests are rejected by server authorization, regardless of the client shell. ## Verify the artifact Run `pnpm pack` and inspect the tarball rather than testing only through workspace links. Install only produced tarballs into a clean external fixture and run prepare, typecheck, build, and migration diagnostics. The repository CI repeats this for pnpm, npm, Yarn, and Bun. Official packages use linked alpha versions and Changesets. Third-party layers choose their own SemVer policy, but should state the supported Nuxt Customer Portal revision or package range and never mutate an already released migration file. # Documentation contributions The documentation site lives in `apps/docs` in the [canonical monorepo](https://github.com/ludulicious/customer-portal){rel=""nofollow""}. Product behavior and documentation now change together in one pull request. ## What good documentation does Every guide should help a reader complete a concrete task or form an accurate mental model. Prefer tested commands, source-backed behavior, small complete examples, explicit role and tenancy assumptions, and links to the next likely task. Avoid documenting planned behavior as shipped behavior. If a capability is experimental, say so where the reader first encounters it. ## Content structure Documentation pages are Markdown files under `content/`. Numeric prefixes control order but are removed from public routes. ```text content/ ├── 1.getting-started/ ├── 2.architecture/ ├── 3.modules/ ├── 4.reference/ ├── 5.operations/ ├── 6.guides/ └── 7.contributing/ ``` Each page needs a clear title and description in frontmatter. Add an icon only when it improves navigation scanning. ## Preview locally ```bash [Terminal] pnpm install pnpm dev ``` Before opening a pull request, run: ```bash [Terminal] pnpm test:docs pnpm lint pnpm typecheck pnpm test:e2e ``` `test:docs` checks required contributor metadata, internal documentation links, repository configuration, and the MCP documentation catalog. `test:e2e` builds the production site and checks the contributor journeys, accessibility, both color themes, and narrow viewports. Also inspect changed code blocks and task-specific interactions. Review `/llms.txt`, `/llms-full.txt`, and the raw Markdown route for the changed page so AI-facing output stays useful. Every maintained page has an **Edit page** link to its source and a visible **Report a docs issue** action. Keep `githubPath` aligned with the file path so the edit link remains accurate. Documentation feedback is tracked in the public documentation repository beside the Markdown source and its documentation issue form. The report action prefills the affected page and the exact Customer Portal source revision against which the documentation was verified, giving maintainers reproducible context without mixing writing problems into the product backlog. Product bugs, feature requests, and module proposals still belong in the Customer Portal repository. The repository URLs, documentation branch, feedback repository, and verified product commit are deployment configuration in `nuxt.config.ts`. Update the product commit whenever a documentation review moves to a newer Customer Portal revision; do not silently describe behavior from a moving branch while displaying an older verification pin. ## Keep docs synchronized When a product pull request changes a public contract, environment variable, route family, feature action, or contributor workflow, update the relevant documentation in the same release cycle. Cross-repository pull requests should link to one another so reviewers can verify both sides. # Testing contributions Testing a Customer Portal contribution means checking its layer boundary as well as its happy path. A feature that works only for one role, one organization, or while tightly coupled to the host is not complete. ## Project checks Run the source-level feature tests first, then the full application checks: ```bash [Terminal] pnpm test:features pnpm validate:feature-locales pnpm lint pnpm typecheck pnpm build ``` `test:features` runs the tests owned by layers. `validate:feature-locales` focuses on the current business features and verifies their locale and contract conventions. ## Authorization matrix For each protected server operation, cover at least: | Context | Expected result | | --------------------------------------------------- | -------------------------------------- | | No session | Rejected | | Member without the action | Rejected | | Owner or organization administrator with the action | Allowed | | Correct user, wrong active organization | Rejected or empty, never leaked | | System administrator | Matches the documented platform bypass | Client-side visibility is a separate UI test; it never replaces handler-level authorization. ## Data and migrations When a contribution changes a schema: 1. Generate and review the root migration. 2. Apply it to an empty database. 3. Apply it to representative pre-change data. 4. Verify every feature-owned table and enum uses the feature schema. 5. Exercise deletion behavior and cross-schema references. 6. Confirm tenant-owned queries always include the active organization. ## Feature contract Test that registered IDs are stable and unique, audiences match the intended roles, module route prefixes activate the right menu, widgets occupy the intended area and size, and every policy action is mapped deliberately. Keep English and Dutch key structures identical. Render important states in both locales, including empty, loading, validation, error, and destructive-confirmation states. ## Portability Build the assembled portal with the layer enabled. Also exclude the layer from a copy of the application and verify that the host still typechecks and builds. Feature removal must not leave imports, navigation entries, widget registrations, locale dependencies, or server references behind. Use [service requests](https://nuxt-customer-portal.com/modules/service-requests) as the compact testing reference and review [tenancy and security](https://nuxt-customer-portal.com/architecture/tenancy-and-security) before changing protected data access. # Propose a module A strong module proposal starts with a shared problem, not a folder structure. Early discussion helps contributors agree on the feature boundary, avoid duplicate work, and identify contracts that belong in portal core. ## Before you propose Search the [Customer Portal issues](https://github.com/ludulicious/customer-portal/issues){rel=""nofollow""} and existing modules. If the capability is specific to one deployment, it may still be valuable as an external layer without becoming part of the default portal. Open the [reusable layer proposal form](https://github.com/ludulicious/customer-portal/issues/new?template=module-proposal.yml){rel=""nofollow""}. GitHub begins the title with `Module proposal:` and asks for the boundary evidence below. ## Proposal outline ```markdown ## Problem Who needs this capability, and what cannot they do today? ## Reusable boundary What belongs in this layer? What is explicitly outside it? ## Audiences and permissions Which users participate? List proposed actions for owner, admin, and member. ## Product surface List routes, navigation, module menus, dashboard widgets, and major states. ## Data ownership Describe tenant-owned records, the PostgreSQL schema, retention, and cross-schema references. ## Integrations List external services, secrets, webhooks, files, or background work. ## Security and privacy Describe sensitive data, organization isolation, destructive actions, and audit needs. ## Delivery plan Suggest reviewable milestones, migrations, tests, translations, and documentation. ``` ## What maintainers will evaluate Discussion should establish whether the capability is reusable, whether it needs a new layer or an existing one, and whether any proposed core change is genuinely cross-cutting. Maintainers will also look for a clear authorization vocabulary, tenant boundaries, migration safety, and a first milestone small enough to review. Agreement on a proposal is not a promise to merge every implementation detail. Keep design choices visible in the issue and link follow-up pull requests so later contributors can understand the reasoning. Once the boundary is clear, follow [create a feature layer](https://nuxt-customer-portal.com/contributing/create-a-layer) and [testing contributions](https://nuxt-customer-portal.com/contributing/testing). # Community and support Customer Portal collaboration currently happens through GitHub issues and pull requests. Choosing the right repository and including useful evidence makes it easier for maintainers and other contributors to respond. ## Where to start | Need | Channel | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Product bug | [Bug report form](https://github.com/ludulicious/customer-portal/issues/new?template=bug-report.yml){rel=""nofollow""} | | Product feature request | [Feature request form](https://github.com/ludulicious/customer-portal/issues/new?template=feature-request.yml){rel=""nofollow""} | | Reusable layer idea | Read [Propose a module](https://nuxt-customer-portal.com/contributing/propose-a-module), then use the [layer proposal form](https://github.com/ludulicious/customer-portal/issues/new?template=module-proposal.yml){rel=""nofollow""} | | Incorrect or unclear documentation | Use **Report a docs issue** on the affected page; the report is tracked in the documentation repository with the page and verified product revision prefilled | | Documentation correction | Use **Edit page** on the affected page and open a focused pull request | | Security vulnerability | Follow the private reporting guidance below; do not open a public issue | | General implementation question | Search the docs and existing issues first, then use the [question form](https://github.com/ludulicious/customer-portal/issues/new?template=question.yml){rel=""nofollow""} with a minimal example | The project does not currently promise a support response time. A clear, reproducible report gives the community the best chance of helping. Documentation reports and pull requests belong in the [documentation repository](https://github.com/ludulicious/customer-portal){rel=""nofollow""}. Product behavior belongs in the [Customer Portal tracker](https://github.com/ludulicious/customer-portal/issues){rel=""nofollow""}. The page action begins documentation issue titles with `Docs:` and includes the source pin automatically, which keeps a report reproducible when the product moves forward. GitHub Discussions are not currently enabled for either repository. Use an issue for a question that could help other users, and keep one issue focused on one problem or proposal. The product repository also publishes [contribution guidelines](https://github.com/ludulicious/customer-portal/blob/master/CONTRIBUTING.md){rel=""nofollow""}, a [support channel map](https://github.com/ludulicious/customer-portal/blob/master/SUPPORT.md){rel=""nofollow""}, a [security policy](https://github.com/ludulicious/customer-portal/blob/master/SECURITY.md){rel=""nofollow""}, and a pull-request checklist. GitHub surfaces these files automatically while someone creates an issue or pull request. ## Useful issue reports Before reporting a problem, reproduce it against a known Customer Portal commit and search for an existing report. Include: - the exact commit from `git rev-parse HEAD`; - the affected layer and route; - user role and organization context, without real customer data; - setup, action, expected result, and actual result; - relevant browser, Node.js, PostgreSQL, and deployment details; - a minimal reproduction, sanitized log excerpt, screenshot, or failing test when possible; - whether the problem occurs with the feature layer removed. Never attach `.env` files, database exports, session cookies, API keys, OAuth secrets, customer invoices, or personal timesheet data. ## Pull requests Open one coherent change per pull request. Explain the user problem, architectural boundary, authorization impact, schema changes, and verification performed. Mark incomplete work as a draft. Review is a technical conversation. Contributors and maintainers should: - discuss the work rather than the person; - make assumptions and tradeoffs explicit; - support corrections with code, tests, documentation, or reproducible behavior; - welcome questions and unfamiliarity with the codebase; - avoid harassment, discrimination, threats, sexualized content, and disclosure of another person's private information; - step away or ask a maintainer to moderate when a discussion stops being constructive. Maintainers may edit, hide, lock, or close participation that makes the project unsafe or persistently unproductive. A formal project-wide code of conduct should be adopted before the first stable release. ## Security Do not describe a suspected vulnerability in a public issue, discussion, pull request, or documentation report. Use GitHub's private vulnerability reporting from the **Security** tab of the affected repository when it is enabled. Report product, authentication, authorization, tenant-isolation, invoice, and timesheet vulnerabilities to the Customer Portal repository. Report vulnerabilities specific to the documentation deployment to the documentation repository. Include the affected commit, impact, prerequisites, reproduction steps, and a suggested mitigation if known. Use synthetic accounts and records; do not test against organizations or data you do not own. ## Current project status Customer Portal is in active development and does not yet publish versioned releases or a support lifecycle. Read [Compatibility and releases](https://nuxt-customer-portal.com/reference/compatibility-and-releases) before depending on an internal API or deploying an upstream update.