Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Void provides a first-class [Drizzle ORM](https://orm.drizzle.team) integration.

Void supports two database backends. Both use the same Drizzle-based workflow for schema definition, querying, and migrations.

`void init` asks which one you want. Choosing D1 keeps the default implicit; choosing PostgreSQL writes `"database": "pg"` to `void.json`.
`void init` can start you with D1, PostgreSQL, or no database yet. Choosing D1 keeps the default implicit; choosing PostgreSQL writes `"database": "pg"` to `void.json`; choosing no database lets you start with static Pages and adopt data features later.

| | [D1 (SQLite)](./database/d1) | [PostgreSQL](./database/postgresql) |
| ---------- | ---------------------------- | -------------------------------------------- |
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/pages-routing/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Pages mode is also entirely optional - you can use any client-side router to bui

## Setup

If you start in a scaffoldable empty directory, `void init` can generate this setup for you: it asks whether you want React, Vue, Svelte, or Solid Pages mode and writes the adapter-aware `vite.config.ts`, `pages/`, and `db/` starter files.
If you start in a scaffoldable empty directory, `void init` can generate this setup for you: it asks whether you want React, Vue, Svelte, or Solid Pages mode, then lets you pick a D1 starter, a PostgreSQL starter, or Static Pages. The database-backed starters write the adapter-aware `vite.config.ts`, `pages/`, and `db/` starter files; Static Pages writes just the basic `pages/` setup so you can grow into server features later.

If you're adding Pages mode manually, install a framework adapter alongside `void`:

Expand Down
4 changes: 1 addition & 3 deletions docs/guide/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ bunx void init

This scaffolds a Pages-mode starter with an interactive setup flow.

As part of `void init`, you'll choose a Pages framework (React, Vue, Svelte, or Solid) and a database. The scaffold writes a framework-specific `vite.config.ts`, a `pages/` home page with a `.server.ts` loader companion, `db/schema.ts`, `db/migrations/0000_initial.sql`, and `routes/api/hello.ts`.

For the database, D1 is the zero-config default for prototyping and read-heavy apps, while PostgreSQL is better when you already have Postgres infrastructure or need heavier writes and more complex queries.
As part of `void init`, you'll choose a Pages framework (React, Vue, Svelte, or Solid) and a starter type. D1 is the default top option and scaffolds a DB-backed page loader, schema, migration, and API route. PostgreSQL scaffolds the same starter and writes `"database": "pg"` to `void.json`. Static Pages skips the database and server starter files so you can start with static content and add Void features later.

<details>
<summary style="cursor:pointer">
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ void init [--tsconfig] [--github] [--agents]

Setup wizard for void projects (new or existing).

If run in a scaffoldable empty directory, `void init` first asks which Pages framework to scaffold (React, Vue, Svelte, or Solid), then asks which database to use (D1 or PostgreSQL). It writes a framework-specific `vite.config.ts`, a `pages/` home page plus `.server.ts` loader, `db/schema.ts`, `db/migrations/0000_initial.sql`, and `routes/api/hello.ts`.
If run in a scaffoldable empty directory, `void init` first asks which Pages framework to scaffold (React, Vue, Svelte, or Solid), then asks which starter you want: D1, PostgreSQL, or Static Pages. The D1 and PostgreSQL starters write a framework-specific `vite.config.ts`, a `pages/` home page plus `.server.ts` loader, `db/schema.ts`, `db/migrations/0000_initial.sql`, and `routes/api/hello.ts`. The Static Pages starter writes just the framework-specific `vite.config.ts` and a `pages/` home page so you can add server features later.

After that, the full interactive flow walks through:

1. **TypeScript:** creates or updates `tsconfig.json`, including `extends .void/tsconfig.json` and `void/env` types.
2. **Database:** asks whether you want D1 or PostgreSQL. Choosing PostgreSQL writes `"database": "pg"` to `void.json`; D1 stays implicit.
2. **Database:** asks whether you want D1, PostgreSQL, or no database yet. Choosing PostgreSQL writes `"database": "pg"` to `void.json`; D1 stays implicit; choosing no database leaves config unchanged so you can add data features later.
3. **Agent instructions:** detects agents once and injects instructions into `CLAUDE.md` or `AGENTS.md`.
4. **Skills:** links Void skills using the same detected or selected agent context.
5. **MCP config:** writes MCP server config using that same agent context.
Expand Down
2 changes: 1 addition & 1 deletion packages/void/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Void ships with first-class support for coding agents (Claude Code, Cursor, Code
- `migrate-voidflare-to-void` — guides migration from `@cloudflare/vite-plugin` to `void`.
- **`docs/`** — all 23 docs markdown files are included in the npm package at `node_modules/void/docs/`.
- **`void init --agents`** — runs all agent setup steps from a single detection/selection pass: injects Void instructions with versioned markers, symlinks skills from the `void` package, and writes MCP config (or prints manual commands/JSON for unsupported/generic agents).
- **`void init` in an empty directory** — scaffolds a Pages-mode starter (React, Vue, Svelte, or Solid), asks for D1 vs PostgreSQL, then runs the same setup flow.
- **`void init` in an empty directory** — scaffolds a Pages-mode starter (React, Vue, Svelte, or Solid), then lets you pick D1, PostgreSQL, or Static Pages before running the same setup flow.

Agent instructions use versioned markers (`<!--injected-by-void-v0.0.1-->` / `<!--/injected-by-void-->`) for safe update-in-place across version upgrades.

Expand Down
133 changes: 106 additions & 27 deletions packages/void/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
SCAFFOLD_FRAMEWORK_OPTIONS,
SCAFFOLD_GITIGNORE_LINES,
type ScaffoldFramework,
type ScaffoldStarterMode,
} from "./template";
import { formatProjectCommand } from "./preset";
import {
Expand Down Expand Up @@ -107,6 +108,7 @@ const VITE_CONFIG_FILES = [
"vite.config.cjs",
];
const VOID_CONFIG_FILE = "void.json";
type DatabaseSetupChoice = DatabaseDialect | "none";

function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
Expand Down Expand Up @@ -408,10 +410,33 @@ function writeVoidConfig(root: string, config: Record<string, unknown>): void {
writeFileSync(join(root, VOID_CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
}

function applyDatabaseDialect(root: string, dialect: DatabaseDialect): void {
function clearConfiguredDatabase(
root: string,
messages: { unchanged: string; updated: string },
): void {
const config = readConfig(root) as Record<string, unknown>;
const configPath = join(root, VOID_CONFIG_FILE);

if (config.database !== "pg") {
log.info(messages.unchanged);
return;
}

delete config.database;

if (Object.keys(config).length === 0) {
unlinkSync(configPath);
log.success(messages.updated);
return;
}

writeVoidConfig(root, config);
log.success(messages.updated);
}

function applyDatabaseDialect(root: string, dialect: DatabaseDialect): void {
const config = readConfig(root) as Record<string, unknown>;

if (dialect === "postgresql") {
if (config.database === "pg") {
log.info("PostgreSQL already configured in void.json");
Expand All @@ -424,38 +449,56 @@ function applyDatabaseDialect(root: string, dialect: DatabaseDialect): void {
return;
}

if (config.database !== "pg") {
log.info("Using default D1 (SQLite) configuration");
return;
}
clearConfiguredDatabase(root, {
unchanged: "Using default D1 (SQLite) configuration",
updated: "Updated void.json to use the default D1 (SQLite) configuration",
});
}

delete config.database;
function detectDatabaseSetup(root: string): DatabaseSetupChoice {
if (getDatabaseDialect(readConfig(root)) === "postgresql") {
return "postgresql";
}
if (
hasDrizzleSchema(root) ||
existsSync(join(root, "db", "migrations")) ||
existsSync(join(root, "migrations"))
) {
return "sqlite";
}
return "none";
}

if (Object.keys(config).length === 0) {
unlinkSync(configPath);
log.success("Using default D1 (SQLite) configuration");
function applyDatabaseSetup(root: string, choice: DatabaseSetupChoice): void {
if (choice === "none") {
clearConfiguredDatabase(root, {
unchanged: "No database configured yet",
updated: "Removed database config from void.json",
});
return;
}

writeVoidConfig(root, config);
log.success("Updated void.json to use the default D1 (SQLite) configuration");
applyDatabaseDialect(root, choice);
}

async function setupDatabaseDialect(root: string): Promise<DatabaseDialect> {
const currentDialect = getDatabaseDialect(readConfig(root));

async function promptStarterMode(): Promise<ScaffoldStarterMode> {
const choice = await select({
message: "Choose a database for this project:",
message: "Choose a Pages starter:",
options: [
{
value: "sqlite",
label: currentDialect === "sqlite" ? "D1 (SQLite) [current]" : "D1 (SQLite)",
hint: "Zero-config and fully managed by Void; best for prototyping and read-heavy apps.",
label: "D1 (SQLite)",
hint: "Recommended starter. Scaffolds a DB-backed page, schema, migration, and API route.",
},
{
value: "postgresql",
label: currentDialect === "postgresql" ? "PostgreSQL [current]" : "PostgreSQL",
hint: "Bring your own Postgres via Hyperdrive; best for write-heavy apps, complex queries, or existing infra.",
label: "PostgreSQL",
hint: 'Scaffolds the same starter with PostgreSQL and writes "database": "pg" to void.json.',
},
{
value: "static",
label: "Static Pages",
hint: "No database or server starter files yet. Good for content sites; add Void features later.",
},
],
});
Expand All @@ -465,7 +508,7 @@ async function setupDatabaseDialect(root: string): Promise<DatabaseDialect> {
process.exit(0);
}

return choice as DatabaseDialect;
return choice as ScaffoldStarterMode;
}

async function promptScaffoldFramework(): Promise<ScaffoldFramework> {
Expand All @@ -482,6 +525,40 @@ async function promptScaffoldFramework(): Promise<ScaffoldFramework> {
return choice as ScaffoldFramework;
}

async function promptDatabaseSetup(root: string): Promise<DatabaseSetupChoice> {
const currentSetup = detectDatabaseSetup(root);
const initialValue = currentSetup === "none" ? "sqlite" : currentSetup;

const choice = await select({
message: "Choose a database setup for this project:",
initialValue,
options: [
{
value: "sqlite",
label: currentSetup === "sqlite" ? "D1 (SQLite) [current]" : "D1 (SQLite)",
hint: "Zero-config and fully managed by Void; best for prototyping and read-heavy apps.",
},
{
value: "postgresql",
label: currentSetup === "postgresql" ? "PostgreSQL [current]" : "PostgreSQL",
hint: "Bring your own Postgres via Hyperdrive; best for write-heavy apps, complex queries, or existing infra.",
},
{
value: "none",
label: "No Database Yet",
hint: "Skip database setup for now. You can add D1 or PostgreSQL later as your app grows.",
},
],
});

if (isCancel(choice)) {
cancel("Setup cancelled.");
process.exit(0);
}

return choice as DatabaseSetupChoice;
}

/** Get the dev command for a detected framework. */
function getDevCommand(root: string, framework?: DetectedFramework): string {
if (!framework) return formatProjectCommand(root, "vite dev");
Expand Down Expand Up @@ -641,12 +718,12 @@ function scaffoldPagesApp(
root: string,
packageManager: ProjectState["packageManager"],
framework: ScaffoldFramework,
dialect: DatabaseDialect,
starterMode: ScaffoldStarterMode,
): void {
const packageJsonPath = join(root, "package.json");
const template = createPagesStarterTemplate({
framework,
dialect,
starterMode,
voidVersion: getOwnVersion(),
});
let pkg: Record<string, any> = {};
Expand Down Expand Up @@ -730,9 +807,11 @@ function scaffoldPagesApp(
}

ensureGitignore(root);
applyDatabaseDialect(root, dialect);
if (starterMode !== "static") {
applyDatabaseDialect(root, starterMode);
}

if (dialect === "postgresql") {
if (starterMode === "postgresql") {
const devCommand = getDevCommand(root);
note(
`Add DATABASE_URL to .env.local before running \`${devCommand}\`.\n` +
Expand Down Expand Up @@ -1033,8 +1112,8 @@ export async function runInitCommand(root: string, initArgs?: InitArgs): Promise
if (!targeted && !framework && isScaffoldableEmptyProject(root)) {
log.info("Empty project detected. Scaffolding a Pages starter...");
scaffoldedPagesFramework = await promptScaffoldFramework();
const dialect = await setupDatabaseDialect(root);
scaffoldPagesApp(root, preScaffoldState.packageManager, scaffoldedPagesFramework, dialect);
const starterMode = await promptStarterMode();
scaffoldPagesApp(root, preScaffoldState.packageManager, scaffoldedPagesFramework, starterMode);
didScaffoldStarter = true;
}

Expand All @@ -1060,7 +1139,7 @@ export async function runInitCommand(root: string, initArgs?: InitArgs): Promise

// Step 3: Database dialect
if (!targeted && !didScaffoldStarter) {
applyDatabaseDialect(root, await setupDatabaseDialect(root));
applyDatabaseSetup(root, await promptDatabaseSetup(root));
}

// Steps 4–6: Agent instructions, skills, MCP config
Expand Down
Loading
Loading