Skip to content

CLI

The vaif command-line tool signs you in to VAIF Studio, scaffolds an app from a template, changes your schema, generates TypeScript types, deploys functions and manages keys and secrets. Run vaif --version to print the installed version and vaif <command> --help for every option.

Terminal window
npm install -g @vaifstudio/cli

Every command takes --api-url <url> for a local or self-hosted API. It defaults to VAIF_API_URL, then https://api.vaif.studio.

Terminal window
vaif login # opens the browser
vaif login --email # email and password in the terminal
vaif login --project-id <id> # also set the default project
vaif whoami
vaif logout

vaif login --email asks for an authenticator or backup code when the account has two-factor sign-in on. The session is saved in ~/.vaif/auth.json, which only your user can read.

A CLI token is a console access token with the cli scope, valid for 30 days. Billing and admin routes refuse it. Get one without a browser with POST /auth/cli/login; see Console API for agents and CI. Then either:

  • Set VAIF_AUTH_TOKEN in the job’s environment, from your CI’s secret store. Every command uses it instead of ~/.vaif/auth.json. It is never written to disk, printed or refreshed, and the CLI ignores a VAIF_AUTH_TOKEN in a .env file, so a cloned repository cannot sign you in as another account.
  • Or save it with vaif login --token, which reads the token from standard input or asks for it without echoing it. The CLI refuses a token written on the command line, where other processes and your shell history could read it. It checks the token with the API before saving it.
Terminal window
vaif login --token < token.txt
Terminal window
vaif init # write vaif.config.json in this folder
vaif init --template react-spa # scaffold an app from a template
vaif init --template react-spa --features auth,database,realtime,storage,functions
vaif init --template react-spa --add-features storage
vaif init --force # overwrite an existing config
vaif templates # list the templates (alias: vaif tpl)

Each template writes a README with setup steps, an environment file example, and starter files for the features you pick: auth, database, realtime, storage and functions. See Templates for what each one includes.

Terminal window
vaif info # name, region and URLs
vaif status # tables, functions, storage and connections
Terminal window
vaif generate # alias: vaif gen
vaif generate --project-id <id> # one project, through the API
vaif generate --output ./src/types/database.ts
vaif generate --dry-run

Where the schema comes from (the first match wins):

  1. --connection <url>: that database, directly.
  2. --project-id <id>: that project, through the API.
  3. database.url in vaif.config.json, then DATABASE_URL: that database, directly. A value that still holds an unexpanded ${...} is skipped.
  4. Otherwise the API, for the project in vaif.config.json, then VAIF_PROJECT_ID, then the vaif login --project-id default.

Through the API, vaif generate also reads the project’s row rules: a column that an insert rule fills from the signed-in user (for example author_id) is optional in the <Table>Insert type, because the server sets it.

Each of these works on one environment of the project: the project’s default environment unless you pass --env <name>.

Terminal window
vaif pull # write the environment's tables to vaif.schema.json
vaif pull --env staging --output ./staging.schema.json
vaif push # show the plan and the SQL, then ask
vaif push --dry-run # only show the plan
vaif push --yes --name add_todos # apply without asking (CI)

vaif push sends the file to the schema engine, prints the plan and the SQL, and asks before applying it as one migration. By default nothing is dropped: tables, columns and indexes that exist only in the database are kept, and a column type change stops the push with a list of what it would change. --allow-destructive applies those changes and drops everything in the environment that the file does not list, tables included.

Pushing to a production environment asks you to type its name; with --yes it also needs --allow-production, and only an owner or admin can do it. If the environment changed after the preview, nothing is applied and you are asked to run the push again.

Terminal window
vaif db push # apply the .sql files in the migrations folder
vaif db push --dir ./migrations
vaif db push --dry-run # what each file would change
vaif db push --env prod --allow-production
vaif db pull --output ./schema.json # the project's current schema

vaif db push sends each file, in order, to the schema engine, the same path the console uses. The engine applies table changes (CREATE TABLE, ADD COLUMN, CREATE [UNIQUE] INDEX, and primary, unique and foreign keys), then runs the file’s INSERT, UPDATE and DELETE statements as your project’s database login. Each file is one transaction: if anything fails, none of it is applied and the push stops. A push never drops or rewrites data: DROP, ALTER COLUMN, views, functions, triggers, sequences, enum types and SQL access policies are refused with the statement number and the reason. A file already applied unchanged is skipped; --force applies it again.

Access to rows is set with row rules, which are closed by default. Put -- vaif:public-read on the line above a CREATE TABLE that signed-out visitors may read, or -- vaif:owner <column> above a table each signed-in user owns, and the table gets that rule.

Terminal window
vaif db seed # every .json or .js file in ./seeds, ./prisma/seed or ./db/seeds
vaif db seed --file ./seeds/users.json --table users
vaif db seed --truncate --yes # empty the tables first
vaif db seed --dry-run

A seed file is an array of rows (the file name is the table), { "table": "users", "data": [...] }, or an object whose keys are table names. Values are sent as query parameters, never pasted into SQL. Tables load in file order and are not rolled back as a whole: if one fails, the CLI stops and lists what was already inserted. Seeding a production environment needs --allow-production.

Terminal window
vaif db reset --dry-run # list what would be dropped
vaif db reset --confirm dev # drop every table, view, sequence, function and type
vaif db reset --env prod --allow-production --confirm <project-slug>

vaif db reset deletes all data in the environment and cannot be undone: only the structure is saved first, not the rows. It needs --confirm with the environment’s name, or asks you to type it. For a production environment it also needs --allow-production and an owner or admin, and the confirmation is the project slug. Every reset, and every refused production attempt, is written to the audit log.

Terminal window
vaif functions deploy # alias: vaif fn deploy
vaif functions deploy --name send-email
vaif functions deploy --runtime typescript
vaif functions deploy --dry-run
vaif functions list # alias: vaif fn ls

See Creating functions.

Terminal window
vaif keys generate --name "Server"
vaif keys list # alias: vaif keys ls

Secrets are encrypted at rest and reach your functions as environment variables. Their values are write-only: they can be replaced, never read back.

Terminal window
vaif secrets set THIRD_PARTY_KEY # asks for the value, hidden
printf '%s' "$THIRD_PARTY_KEY" | vaif secrets set THIRD_PARTY_KEY --from-stdin # CI
vaif secrets set SERVICE_ACCOUNT --from-file ./service-account.json
vaif secrets list # names only (alias: vaif sec ls)
vaif secrets delete THIRD_PARTY_KEY

A value is never taken from the command line, where other programs and your shell history could read it. --from-stdin drops one trailing newline. vaif secrets get explains that values are write-only and exits with status 1. Every secrets command takes --project-id and --env-id. A secret for a production environment, or one for every environment (--all-environments), needs --allow-production and an owner or admin.

Inside a function, a secret is an environment variable:

export default async function handler(req: Request): Promise<Response> {
const key = process.env.THIRD_PARTY_KEY;
return Response.json({ configured: Boolean(key) });
}
Terminal window
vaif migrate --from supabase --dry-run # writes vaif-migration-plan.md and creates nothing
vaif migrate --from supabase --project-id <id>
vaif migrate --from firebase --dry-run

A migration creates the tables, buckets and function stubs in your project. Data, users and function code are not copied. --output-dir <dir> points the CLI at another project folder. See Migrate from Supabase and Migrate from Firebase.

  • vaif workspace pull --session <id> --output <dir> downloads an accepted Verament session as a local project. --open code opens it in your editor.
  • To connect an AI coding tool to your project, see MCP server, which links the setup for each tool.

vaif init writes vaif.config.json:

{
"projectId": "your-project-id",
"database": { "url": "${DATABASE_URL}", "schema": "public" },
"types": { "output": "./src/types/database.ts" },
"api": { "baseUrl": "https://api.vaif.studio" }
}
Variable What it does
VAIF_AUTH_TOKEN A CLI token used instead of ~/.vaif/auth.json. Read from the environment only, never from .env
VAIF_PROJECT_ID The default project
VAIF_API_URL The API’s address (default https://api.vaif.studio)
DATABASE_URL A direct database connection for vaif generate

The project is chosen in this order: --project-id, then vaif.config.json, then VAIF_PROJECT_ID, then the vaif login --project-id default. A .env file in the current folder is loaded first; a variable already set in your shell wins.