Veap does not force a rigid directory layout, but scaffolded applications and official plugins follow a predictable convention so the veap CLI, migrations, and virtual router work together with zero boilerplate.
Typical directory layout#
A full-stack Next.js and Veap application is organized as follows:
my-app/
├── app/
│ ├── (site)/ # Application-owned public routes and layouts
│ ├── [...catchAll]/
│ │ └── page.tsx # Mounts @veap/plugins Virtual Router pages
│ ├── api/
│ │ └── [...catchAll]/
│ │ └── route.ts # Mounts @veap/plugins Virtual Router API routes
│ ├── globals.css # Tailwind CSS v4 + @veap/ui/globals.css
│ └── layout.tsx # Root HTML shell, ThemeProvider, I18nProvider
├── components/ # Application React components and layouts
├── lib/
│ ├── plugins.gen.ts # Auto-generated plugin imports (managed by CLI)
│ └── veap.ts # Application composition root and initializeSystem()
├── locales/
│ ├── en.json # English ICU translation dictionary
│ └── de.json # German ICU translation dictionary
├── migrations/
│ └── index.ts # Application-level database migrations
├── plugins/ # Local or ejected plugins (created by veap eject)
├── instrumentation.ts # Next.js server startup hook
├── next.config.ts # Next.js configuration
├── veap.config.ts # Veap framework configuration
└── package.jsonFramework configuration (veap.config.ts)#
Place a veap.config.ts file at the root of your project to configure framework-wide defaults such as the private admin path prefix and supported locales:
// veap.config.ts
import type { VeapConfig } from "@veap/kernel";
const config: VeapConfig = {
privatePath: "/app",
intl: {
cookie: "VEAP_LOCALE",
default: "en",
locales: ["en", "de"],
timeZone: "UTC",
},
debug: false,
};
export default config;Configuration properties#
VeapConfig supports the following settings:
| Property | Type | Default | Description |
|---|---|---|---|
privatePath | string | "/app" | URL prefix used by panel and admin plugins for protected routes. |
intl.cookie | string | "NEXT_LOCALE" | Cookie name used by @veap/intl to persist the active user locale. |
intl.default | string | "en" | Fallback locale when no cookie or Accept-Language match is found. |
intl.locales | string[] | ["en"] | List of locale codes supported by the application. |
intl.timeZone | string | "UTC" | Default IANA time zone for date and time formatting. |
debug | boolean | false | Enables verbose diagnostic logging across kernel services. |
Environment variables (.env)#
Runtime secrets and infrastructure connection strings belong in .env (or your deployment platform's environment settings) and are read through ConfigService:
# Database connection (better-sqlite3, pg, or mysql2)
DB_CLIENT=better-sqlite3
DATABASE_URL=./data/app.sqlite
# Application URL
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Mail transport (console, smtp, or custom)
MAIL_TRANSPORT=console
MAIL_FROM_ADDRESS=noreply@example.com
# Local file storage directory
FILE_STORAGE_FOLDER=public/uploadsGenerated plugin registry (lib/plugins.gen.ts)#
When you install or remove plugins using the veap CLI (bun veap add <plugin> or bun veap remove <plugin>), Veap automatically updates lib/plugins.gen.ts:
// lib/plugins.gen.ts
// This file is auto-generated by Veap CLI. Do not edit manually.
import type { IPlugin } from "@veap/plugins";
import authPlugin from "@veap/auth-plugin";
import panelPlugin from "@veap/panel-plugin";
export const plugins: IPlugin[] = [authPlugin, panelPlugin];Import plugins in lib/veap.ts and pass it to PluginsServiceProvider and .withPlugins(plugins) so newly installed plugins register their routes, migrations, translations, and widgets automatically.
Catch-all routes for plugins#
If your application uses @veap/plugins to render virtual plugin pages (such as /signin from @veap/auth-plugin or /app from @veap/panel-plugin), mount the virtual router in app/[...catchAll]/page.tsx:
// app/[...catchAll]/page.tsx
import { buildRouteTree, VeapRouter } from "@veap/plugins/router";
import type { Metadata } from "next";
import { initializeSystem } from "@/lib/veap";
export const dynamic = "force-dynamic";
export default async function CatchAllPage({
params,
searchParams,
}: {
params: Promise<{ catchAll?: string[] }>;
searchParams: Promise<Record<string, string>>;
}) {
const { catchAll } = await params;
const resolvedSearchParams = await searchParams;
const path = catchAll && catchAll.length > 0 ? `/${catchAll.join("/")}` : "/";
await initializeSystem();
const tree = await buildRouteTree(true);
return VeapRouter({
tree,
path,
searchParams: resolvedSearchParams,
});
}Because Next.js resolves explicit file-system routes in app/ before falling back to app/[...catchAll]/page.tsx, any physical page you create in app/ naturally overrides a plugin route at the same URL.
Customizing plugins with veap eject#
When you need full ownership over an installed plugin's UI, routes, or business logic, you can eject it into your application's local plugins/ directory:
bun veap eject @veap/blog-pluginThis command clones the plugin's source repository into plugins/blog-plugin/, configures "workspace:*" in package.json, and links it via your package manager so you can customize every component and model locally without changing any package import specifiers.
Next steps#
Now that you know how a Veap project is structured, explore the @veap/kernel fundamentals:
- IoC container and service providers — Learn how tokens, constructor injection,
ServiceProviderclasses, andApplicationwork under the hood.