One of the most AI-friendly full-stack blueprints available today — now with Bun + Elysia speed, Drizzle simplicity, full TypeScript support, and Nuxt 4 SSR/SSG.
Vasp is a batteries-included, declarative full-stack web framework for Vue developers. Write a single main.vasp configuration file and get a complete, production-ready application — backend, frontend, auth, database schema, realtime, and background jobs — all generated automatically.
| Layer | Technology | Why |
|---|---|---|
| Runtime | Bun | Fastest JS runtime available |
| Backend | Elysia | Fastest framework on Bun |
| ORM | Drizzle | Lightest bundle, fastest queries |
| Frontend | Vue 3 + Vite (SPA) or Nuxt 4 (SSR/SSG) | Best DX + SSR/SSG |
| HTTP Client | ofetch ($vasp composable) |
Isomorphic, works in SPA, SSR, and Nitro with no shimming |
| Language | JavaScript (default) + TypeScript (opt-in) | Maximum flexibility |
| Config DSL | .vasp files |
Single source of truth |
# Install Vasp CLI
bun install -g vasp-cli
# Create a new app — interactive prompts guide you through TypeScript, SSR and starter selection
vasp new my-app
# Or pass flags directly to skip prompts
vasp new my-app --typescript --ssr
vasp new my-app --starter=todoThat's it. Vasp generates your entire stack in under 5 seconds.
Every project starts with a single main.vasp file:
app MyTodoApp {
title: "Vasp Todo"
db: Drizzle
ssr: false // false = SPA (default), true = SSR, "ssg" = Static Site Generation
typescript: false // false = pure JS (default), true = TypeScript
env: {
DATABASE_URL: required
JWT_SECRET: required
GOOGLE_CLIENT_ID: optional
}
}
auth User {
userEntity: User
methods: [ usernameAndPassword, google, github ]
}
route HomeRoute {
path: "/"
to: HomePage
}
page HomePage {
component: import Home from "@src/pages/Home.vue"
}
query getTodos {
fn: import { getTodos } from "@src/queries.js"
entities: [Todo]
}
action createTodo {
fn: import { createTodo } from "@src/actions.js"
entities: [Todo]
}
entity Todo {
id: Int @id
title: String
done: Boolean
createdAt: DateTime @default(now)
}
crud Todo {
entity: Todo
operations: [list, create, update, delete]
}
realtime TodoChannel {
entity: Todo
events: [created, updated, deleted]
}
job sendWelcomeEmail {
executor: PgBoss
perform: {
fn: import { sendWelcomeEmail } from "@src/jobs.js"
}
schedule: "0 * * * *"
}
admin {
entities: [Todo, User]
}
Vasp reads this file and generates a complete full-stack application. You only write business logic — everything else is handled.
Add an admin block to instantly generate a PrimeVue 4 admin panel wired to your CRUD endpoints:
admin {
entities: [Todo, User]
}
The admin panel is generated as a lazy-loaded route group at /admin inside your main SPA or SSR application — no separate process or port required:
- Collapsible sidebar with a route per entity and a dashboard overview
- Per-entity CRUD table — server-paginated, sortable, with inline Delete confirmation
- Create / Edit modal — form fields are automatically typed (toggle for Boolean, number input for Int/Float, textarea for Text, text input for String, etc.)
- Native
fetchAPI client per entity, hitting your Elysia backend at/api/admin - Supports both JavaScript and TypeScript (controlled by
app.typescript)
The admin panel starts automatically with the rest of your app:
vasp start # admin panel is available at http://localhost:5173/adminVasp generates one of two clearly distinct frontend targets based on the ssr flag.
- Pure Vue 3 + Vite + Vue Router
- Auth guards via Vue Router
beforeEachhooks - Ideal for dashboards, admin panels, and authenticated apps
- Full Nuxt 4 project —
nuxt.config.ts, Nitro engine, hybrid rendering - File-based routing via
pages/, typed server routes viaserver/api/ - Auth guards via Nuxt middleware (
defineNuxtRouteMiddleware) - SSG pre-renders at build time; SSR renders per-request via Nitro
Switch modes at any time:
vasp enable-ssrentity Post {
id: Int @id
slug: String @unique
title: String @validate(minLength: 1, maxLength: 255)
email: String @validate(email)
website: String @validate(url)
score: Float @validate(min: 0, max: 100)
body: Text @nullable
meta: Json @nullable
status: Enum(draft, published, archived)
avatar: File @storage(UserFiles) // requires a storage block named UserFiles
author: User @onDelete(cascade) // cascade | restrict | setNull
createdAt: DateTime @default(now)
updatedAt: DateTime @updatedAt
}
entity Product {
id: Int @id
sku: String
name: String
price: Float
@@index([sku]) // single-column index
@@unique([sku]) // single-column unique constraint
@@index([name], type: fulltext) // GIN full-text search index
}
Inline field config and CRUD sub-blocks give you per-field UI metadata and rich list/form control without any extra code:
entity Todo {
id: Int @id
title: String {
label: "Task Title"
placeholder: "Enter a task name…"
description: "The primary text of the todo item"
validate {
minLength: 3
maxLength: 120
required: true
}
}
priority: Enum(low, medium, high) {
label: "Priority"
default: "medium"
}
done: Boolean { label: "Completed" default: false }
createdAt: DateTime @default(now)
}
crud Todo {
entity: Todo
operations: [list, create, update, delete]
list {
paginate: true
sortable: [title, createdAt, priority]
filterable: [done, priority]
search: [title]
columns {
title { label: "Task" width: "40%" sortable: true }
priority { label: "Priority" width: "120px" filterable: true }
done { label: "Done" width: "80px" }
}
}
form {
layout: "2-column" // 1-column | 2-column | tabs | steps
sections {
basics { label: "Basic Info" fields: [title, priority] }
status { label: "Status" fields: [done] }
}
}
permissions {
list: [admin, user]
create: [admin, user]
update: [admin]
delete: [admin]
}
}
Upgrade an existing v1 file automatically:
vasp migrate # creates main.vasp.bak, rewrites flat props to nested blockscrud Post {
entity: Post
operations: [list, create, update, delete]
list: {
paginate: true
sortable: [createdAt, title]
filterable: [status]
search: [title, body]
}
}
api webhookReceiver {
method: POST
path: "/api/webhooks/stripe"
fn: import { handleStripeWebhook } from "@src/webhooks.js"
}
api adminStats {
method: GET
path: "/api/admin/stats"
fn: import { getStats } from "@src/admin.js"
auth: true
roles: [admin]
}
middleware requestLogger {
fn: import { logRequest } from "@src/middleware.js"
scope: global // global | route
}
cache QueryCache {
provider: redis // memory | redis | valkey
ttl: 300 // default TTL in seconds
redis: {
url: REDIS_URL // env var name holding the connection URL
}
}
query getPublicPosts {
fn: import { getPublicPosts } from "@src/queries.js"
entities: [Post]
cache: {
store: QueryCache
ttl: 600
key: "public-posts"
invalidateOn: ["Post:create", "Post:update", "Post:delete"]
}
}
email Mailer {
provider: resend // resend | sendgrid | smtp
from: "[email protected]"
templates: [
{ name: welcome; fn: import { welcomeEmail } from "@src/emails.js" }
]
}
action registerUser {
fn: import { registerUser } from "@src/actions.js"
entities: [User]
onSuccess: {
sendEmail: welcome
}
}
storage UserFiles {
provider: s3 // local | s3 | r2 | gcs
bucket: my-bucket
maxSize: "10mb"
allowedTypes: ["image/jpeg", "image/png"]
publicPath: "/uploads"
}
seed {
fn: import { seed } from "@src/seed.js"
}
Run with vasp db seed.
app MyApp {
title: "My App"
db: Drizzle
ssr: false
typescript: true
env: {
DATABASE_URL: required String
JWT_SECRET: required String @minLength(32)
PORT: optional Int @default(3001)
NODE_ENV: optional Enum(development, production, test) @default(development)
ALLOWED_ORIGINS: optional String @startsWith("https://")
}
}
app MyApp {
title: "SaaS App"
db: Drizzle
ssr: false
typescript: true
multiTenant: {
strategy: row-level // row-level | schema-level | database-level
tenantEntity: Workspace
tenantField: workspaceId
}
}
auth UserAuth {
userEntity: User
methods: [usernameAndPassword]
roles: [admin, editor, viewer]
permissions: {
"post:create": [admin, editor]
"post:delete": [admin]
"post:read": [admin, editor, viewer]
}
}
query getPosts {
fn: import { getPosts } from "@src/queries.js"
entities: [Post]
auth: true
roles: [admin, editor]
}
crud Post {
entity: Post
operations: [list, create, update, delete]
permissions: {
list: "post:read"
create: "post:create"
update: "post:create"
delete: "post:delete"
}
}
All data access goes through a single isomorphic composable powered by ofetch:
const { $vasp } = useVasp()
// Works identically in SPA, SSR server render, and SSG
const todos = await $vasp.query('getTodos')
await $vasp.action('createTodo', { text: 'Buy milk' })In SSR mode, a single universal Nuxt plugin (plugins/vasp.js) handles both server and client. During SSR renders the plugin forwards the incoming request cookies to the Elysia backend via useRequestHeaders, so the user session is preserved. On the client after hydration, cookies are sent via credentials: 'include'. The developer sees one API regardless of mode.
Reactive authentication composable. Auto-fetches the current user on creation:
import { useAuth } from '@vasp-framework/runtime'
const { user, isAuthenticated, login, register, logout } = useAuth()Returns reactive user, loading, error, isAuthenticated, and methods for login(), register(), logout(), and refresh().
my-app/
├── main.vasp ← Your entire app declaration
├── vasp.config.ts ← Optional plugin configuration (custom generators, template overrides)
├── README.md ← Project documentation
├── .env ← Working environment config
├── .env.example ← Template for environment variables
├── src/
│ ├── pages/ ← Your Vue page components
│ ├── components/
│ ├── queries.js/.ts ← Your query implementations
│ ├── actions.js/.ts ← Your action implementations
│ ├── jobs.js/.ts ← Your background job handlers
│ ├── lib/
│ └── admin/ ← Only when admin block is present
│ ├── AdminLayout.vue
│ ├── views/
│ │ ├── Dashboard.vue
│ │ └── {Entity}/
│ │ ├── index.vue ← Per-entity CRUD table
│ │ └── FormModal.vue ← Create / Edit modal
│ └── api/ ← Per-entity fetch clients
├── server/ ← Generated Elysia backend
│ ├── index.{js|ts}
│ ├── middleware/
│ │ └── rateLimit.{js|ts}
│ ├── db/
│ │ └── client.{js|ts}
│ └── routes/
│ ├── queries/
│ ├── actions/
│ ├── crud/
│ ├── realtime/
│ └── jobs/
├── drizzle/
│ └── schema.js/.ts ← Auto-generated Drizzle schema
├── nuxt/ ← Only when ssr/ssg enabled
├── bunfig.toml
├── vite.config.js ← or nuxt.config.ts when SSR/SSG
├── tsconfig.json ← Only when typescript: true
└── package.json
vasp new <name> [--typescript] [--ssr] [--ssg] [--starter=<name>] [--no-install]
# Interactive prompts shown when no flags are provided
vasp add entity <Name> # Add an entity block + Drizzle column
vasp add page <Name> # Add a page + route + Vue component stub
vasp add crud <Entity> # Add CRUD endpoints for an entity
vasp add query <name> # Add a query block + function stub
vasp add action <name> # Add an action block + function stub
vasp add job <name> # Add a background job + function stub
vasp add auth # Add auth block (+ User entity if missing)
vasp add api <name> # Add a custom API endpoint + handler stub
vasp generate [--force] [--dry-run] # Regenerate from main.vasp (preserves user changes)
vasp validate [--watch] [--strict] # Validate main.vasp without generating
vasp start # Start dev server (auto-migrates schema, opens browser)
vasp build # Production build
vasp db push # Push schema to database
vasp db generate # Generate a migration
vasp db migrate # Run pending migrations
vasp db studio # Open Drizzle Studio
vasp db seed # Seed the database
vasp migrate # Upgrade v1 .vasp file to v2 nested DSL syntax
vasp migrate-to-ts # Upgrade an existing JS project to TypeScript
vasp enable-ssr # Switch a SPA project to SSR/SSG
vasp deploy --target=<docker|fly|railway> # Generate deployment config files
vasp eject # Remove Vasp framework dependency (one-way)
vasp --version| Feature | Status |
|---|---|
.vasp DSL parser + validator |
Done |
| Code generator (all block types) | Done |
| Entity DSL with typed fields & modifiers | Done |
| SPA scaffold (Vue 3 + Vite) | Done |
| SSR/SSG scaffold (Nuxt 4) | Done |
| JavaScript and TypeScript | Done |
| Elysia backend generation | Done |
| Drizzle schema generation (entity-aware) | Done |
| Auth (username/password, Google, GitHub) | Done |
useAuth() composable |
Done |
| Queries and Actions | Done |
| CRUD endpoints (with pagination/sorting) | Done |
| Realtime (WebSocket with auth & rooms) | Done |
| Background jobs (PgBoss, BullMQ with cron scheduling) | Done |
Auto-generated pages (autoPage block — list/form/detail powered by PrimeVue 4) |
Done |
| Admin panel generation (PrimeVue 4, per-entity CRUD UI) | Done |
vasp new CLI command |
Done |
vasp new interactive prompts (TypeScript / SSR / starter) |
Done |
vasp new --starter=<name> |
Done |
vasp add — incremental block scaffolding (8 sub-commands) |
Done |
vasp db commands |
Done |
vasp migrate-to-ts |
Done |
vasp enable-ssr |
Done |
| Rate limiting (IP-based, configurable) | Done |
app.env schema + startup env validation |
Done |
Pre-flight checks in vasp start |
Done |
Auto-generated .env and README.md |
Done |
vasp generate (safe regeneration with manifest) |
Done |
vasp start dev server (schema auto-migration, browser auto-open) |
Done |
vasp build |
Done |
vasp deploy (Docker, Fly.io, Railway) |
Done |
vasp eject (remove framework dependency) |
Done |
OpenAPI / Swagger UI (/api/docs) |
Done |
| Entity relations (foreign keys + Drizzle relations) | Done |
| Enum field type | Done |
Rich field modifiers (@nullable, @default, @updatedAt) |
Done |
| Valibot validation schemas (server + client) | Done |
Structured error envelope ({ ok, data, error }) |
Done |
| Request tracing + dev logger | Done |
| Test scaffold generation (Vitest) | Done |
File storage (storage block — S3, R2, GCS, local) |
Done |
Email (email block — Resend, SendGrid, SMTP) |
Done |
Query caching (cache block — memory, Redis, Valkey) |
Done |
Custom API endpoints (api block) |
Done |
Custom middleware (middleware block — global/route scope) |
Done |
Database seeding (seed block) |
Done |
| Multi-tenancy (row-level, schema-level, database-level) | Done |
| Role-based access control (roles + permissions on auth/query/action/crud) | Done |
Field validation (@validate — email, url, uuid, minLength, maxLength, min, max) |
Done |
Entity-level indexes (@@index, @@unique, fulltext) |
Done |
v2 DSL nested entity field config (label, placeholder, validate {}) |
Done |
v2 DSL nested CRUD sub-blocks (list {}, form {}, columns {}, sections {}, steps {}) |
Done |
vasp migrate — auto-upgrade v1 → v2 DSL syntax |
Done |
Plugin system (vasp.config.ts — custom generators, template overrides, Handlebars helpers) |
Done |
| VS Code extension (syntax highlighting, snippets, diagnostics, completions, hover, go-to-definition) | Done |
Language server (@vasp-framework/language-server — real-Lexer-based LSP, multi-file workspace) |
Done |
Install the Vasp VS Code extension for first-class editor support:
# Install from the VS Code Marketplace (once published)
code --install-extension vasp-framework.vasp-vscode
# Or install from a locally built .vsix (see packages/vscode-extension/README.md)
code --install-extension vasp-vscode-0.1.0.vsixFeatures:
| Feature | Details |
|---|---|
| Syntax highlighting | All 20 block types, @modifiers, primitive/executor/provider types, strings, booleans |
| Code snippets | 22 snippets with tab-stops for every block type |
| Diagnostics | Real-time parse errors + semantic validation (300 ms debounce) |
| Completions | 15 context states — entity names, page names, executor/provider enums with auto-insert snippets |
| Hover docs | Inline Markdown documentation for every keyword |
| Go-to-Definition | Jump to entity/page declarations across all .vasp files in the workspace |
Building the extension from source:
# 1. Build the language server
cd packages/language-server && bun run build
# 2. Build the extension host
cd packages/vscode-extension && bun run build
# 3. Copy the language server into the extension directory
mkdir -p packages/vscode-extension/language-server
cp -r packages/language-server/dist packages/vscode-extension/language-server/dist
# 4. Package as .vsix
cd packages/vscode-extension && npx @vscode/vsce package
# 5. Install
code --install-extension packages/vscode-extension/vasp-vscode-0.1.0.vsixSee packages/vscode-extension/README.md for the complete build guide including dev-mode F5 launch config and Marketplace publishing instructions.
Vasp is a Bun monorepo with the following packages:
| Package | Description |
|---|---|
vasp (CLI) |
The vasp binary — vasp new, vasp migrate, vasp migrate-to-ts, etc. |
@vasp-framework/parser |
Lexer, parser, and semantic validator for the .vasp DSL (v2 nested syntax) |
@vasp-framework/generator |
Generates Elysia backend, Vue/Nuxt frontend, and client SDK from a parsed AST |
@vasp-framework/core |
Shared types, AST definitions, and error classes |
@vasp-framework/runtime |
Runtime composables (useVasp, useQuery, useAction, useAuth) shipped into generated apps |
@vasp-framework/language-server |
LSP server — diagnostics, completions, hover, go-to-definition |
vasp-vscode |
VS Code extension — syntax highlighting, snippets, LSP client |
Prerequisites: Bun >= 1.0
git clone https://github.com/your-org/vasp
cd vasp
bun install
# Run all tests
bun run test
# Run tests in watch mode
bun run test:watch
# Type check
bun run typecheck
# Build all packages
bun run buildTests are written with Vitest and cover the parser, semantic validator, generator, and template engine. E2E fixture files live in e2e/fixtures/.
-
Parse — The
.vaspsource is tokenized by theLexerand parsed into aVaspASTby theParser. TheSemanticValidatorthen checks for semantic correctness (duplicate names, invalid references, etc.). -
Generate — The
generate()function walks the AST and runs a pipeline of specialized generators in dependency order:ScaffoldGenerator→ project skeleton and config filesDrizzleSchemaGenerator→ Drizzle schema from entity declarations (typed columns, enums, relations)BackendGenerator→ Elysia server entry point, rate-limit/CSRF middleware, startup env validationObservabilityGenerator→ OpenTelemetry tracing, Prometheus/OTLP metrics, structured logging, error trackingAuthGenerator→ JWT auth middleware, login/register routes, Login/Register Vue componentsMiddlewareGenerator→ custom middleware blocks (global or route-scoped)CacheGenerator→ cache store setup (memory, Redis, Valkey)QueryActionGenerator→ typed query and action endpointsApiGenerator→ custom API endpointsCrudGenerator→ REST CRUD endpoints with pagination, sorting, filteringRealtimeGenerator→ WebSocket channels with auth and room broadcastingAutoPageGenerator→ list/form/detail pages fromautoPageblocks (PrimeVue 4)JobGenerator→ PgBoss/BullMQ/RedisStreams/RabbitMQ/Kafka background job wiring and cron schedulingEmailGenerator→ email provider setup (Resend, SendGrid, SMTP)SeedGenerator→ database seed scriptStorageGenerator→ file upload endpoints (S3, R2, GCS, local)WebhookGenerator→ inbound webhook receivers + outbound CRUD event dispatchersFrontendGenerator→ Vue 3 SPA (Vite) or Nuxt 4 SSR/SSGAdminGenerator→ PrimeVue 4 admin panel integrated at/adminas a lazy-loaded route group (whenadminblock is present)
All output files are produced from Handlebars templates under
templates/. There are four frontend template trees (SPA+JS, SPA+TS, SSR+JS, SSR+TS) plus a shared backend tree and an admin tree. -
Run —
bunruns the generated backend and frontend simultaneously with hot reload.
Vasp's code-generation pipeline is fully extensible without forking the monorepo. Create a vasp.config.ts (or vasp.config.js) at your project root and export a plugins array. vasp generate and vasp start load this file automatically.
// vasp.config.ts
import type { VaspPlugin } from "@vasp-framework/core";
const myPlugin: VaspPlugin = {
name: "acme-plugin",
// 1. Custom generators — run after all 19 built-in generators
generators: [
{
name: "VersionFileGenerator",
run(ctx, write) {
// ctx exposes the parsed AST and output-mode flags (isTypeScript, isSpa, etc.)
write(
`src/version.${ctx.ext}`,
`export const APP_TITLE = "${ctx.ast.app?.title}";\n`,
);
},
},
],
// 2. Template overrides — replace any built-in Handlebars template
templateOverrides: {
// Key = path relative to templates/ root; value = raw .hbs source
"shared/server/index.hbs": `// My company's custom Elysia entry\n{{appName}}`,
},
// 3. Custom Handlebars helpers — available in all templates (including overrides)
helpers: {
shout: (str: unknown) => String(str).toUpperCase() + "!!!",
},
};
export default { plugins: [myPlugin] };| Extension | How it works |
|---|---|
generators |
Array of { name, run(ctx, write) } objects. Each runs after all built-in generators. Use the write(relativePath, content) callback to emit files — it records every file in the manifest so incremental generation and orphan-deletion stay consistent. |
templateOverrides |
A Record<string, string> where keys are template paths relative to the templates/ root (e.g. "shared/server/index.hbs") and values are raw Handlebars source strings. Overrides take effect after the built-in template directory is loaded, so they win over defaults. All built-in template data (appName, isTypeScript, entity lists, etc.) is available in overridden templates. |
helpers |
A Record<string, fn> of custom Handlebars helpers registered before any template is rendered. Keys become helper names (e.g. "shout" → {{shout name}}). The trailing Handlebars options object is stripped automatically so your function receives only the declared arguments. |
interface PluginGeneratorContext {
ast: VaspAST; // fully-parsed main.vasp
projectDir: string; // absolute path to the real project directory
isTypeScript: boolean;
isSsr: boolean;
isSsg: boolean;
isSpa: boolean;
ext: "ts" | "js"; // file extension for generated source files
}The write callback validates that relativePath resolves inside the project directory — path-traversal attempts (e.g. ../../etc/passwd) are rejected with an error.
Generated applications include production-grade defaults out of the box:
- CORS — Configurable cross-origin resource sharing via
@elysiajs/cors - Rate limiting — IP-based sliding-window limiter (configurable via
RATE_LIMIT_MAXandRATE_LIMIT_WINDOW_MSenv vars, defaults to 100 requests per 60 seconds) - CSRF protection — CSRF middleware generated in
server/middleware/csrf.{js|ts} - Password hashing — Argon2id via
Bun.password.hash()for auth password storage - Auth middleware — JWT-based authentication with cookie transport
- Input validation — Elysia type-safe body/query validation on CRUD endpoints
- Startup env validation — typed
app.envdeclarations checked at server startup with clear error messages
Apache License 2.0. See LICENSE.