A generated Veap application is a workspace. The host Next.js app is the root workspace; plugins, templates and the framework live in sibling packages.
my-app/
├── app/ # Next.js App Router shell (see below)
├── components/ # App-level React components
├── lib/
│ ├── veap.ts # Composition root: Application builder + bootstrap
│ └── plugins.gen.ts # Generated plugin registry (do not edit)
├── plugins/ # Local plugins, one package per directory
│ └── my-plugin/
├── templates/ # Local templates, one package per directory
├── migrations/ # App migrations (index.ts is generated)
├── locales/ # App-level translation files (en.json, pl.json, ...)
├── public/
│ └── storage/ # Default local file storage folder
├── proxy.ts # Next.js proxy (formerly middleware.ts): sets x-pathname header
├── veap.config.ts # Veap application configuration
├── next.config.ts # Next.js configuration (transpiles @veap/* packages)
├── package.json
└── tsconfig.jsonThe app/ directory#
The generated Next.js shell is intentionally small. Veap owns a few files in it:
app/
├── layout.tsx # Root layout: boots Veap, I18nProvider, AppProvider, extension points
├── not-found.tsx # Global 404 page
├── error.tsx # Global error boundary (client component)
├── globals.css
├── [[...catchAll]]/
│ └── page.tsx # Optional catch-all: forwards every page URL to VeapRouter
├── api/[...catchAll]/
│ └── route.ts # Catch-all API route: forwards every /api URL to plugin API routes
└── storage/[...path]/
└── route.ts # Serves files from public/storage with directory-traversal protectionThe optional catch-all [[...catchAll]] handles every URL that no physical Next.js page handles. It is the entry point of the virtual router. Two consequences worth knowing:
- A physical
app/page.tsxwould collide with the optional catch-all (both claim/). The scaffolder clears the CNAapp/directory before copying its own files; do not re-add a rootpage.tsx. - Route files that Veap generates (
layout.tsx, the catch-alls,not-found.tsx) setexport const dynamic = "force-dynamic". Keep it: the root layout boots Veap at request time, and prerendering these routes duringnext buildbreaks context binding. This trade-off is documented in the framework decision records (ADR-006).
lib/veap.ts, the composition root#
This is the file where you decide which framework features and plugins your application uses:
import { cache } from "react";
import { Application } from "@veap/core/core/server";
import { default as MinimalTemplate } from "@veap/minimal-template";
import { appMigrations } from "../migrations";
import { plugins } from "./plugins.gen";
export const app = Application.configure()
.withDatabase()
.withAuth()
.withStorage()
.withCommunication()
.withIntl()
.withRouter()
.withSettings()
.withMigrations(appMigrations)
.withPlugins(plugins)
.withTemplates([MinimalTemplate])
.create();
export const initializeSystem = cache(async () => {
return app.bootstrap();
});Every with* call registers the corresponding service providers. Remove a call and that subsystem is not registered; add your own providers with .withProviders([...]).
initializeSystem wraps app.bootstrap() in React's cache, so it runs once per request no matter how many components call it. It is called at the top of the root layout and of both catch-all routes.
lib/plugins.gen.ts#
Generated by the CLI (veap register, or automatically by veap add and veap make:plugin). It imports every installed plugin package and exports the array passed to .withPlugins(plugins). Do not edit it by hand.
veap.config.ts#
Application configuration read at runtime (server only). Currently supports the admin path prefix and intl options:
import type { VeapConfig } from "@veap/core/core";
const config: VeapConfig = {
privatePath: "/app",
intl: {
default: "en",
locales: ["en", "pl"],
timeZone: "Europe/Warsaw",
},
};
export default config;All options are described in Configuration.