
Insforge Cli
- 23.6k installs
- 33 repo stars
- Updated July 27, 2026
- insforge/agent-skills
insforge-cli is a command-line interface for managing InsForge backend infrastructure, databases, deployments, and cloud services.
About
InsForge CLI for managing backend infrastructure and cloud configuration - database migrations, RLS policies, edge functions, storage, payments, deployments, secrets, and diagnostics. Use when managing InsForge backend projects through the command line.
- Database migrations, RLS policies, functions, storage buckets, secrets, schedules, and diagnostics
- Full backend lifecycle: project setup, schema management, deployments, branching, and logs
Insforge Cli by the numbers
- 23,588 all-time installs (skills.sh)
- +3,092 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #44 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
insforge-cli capabilities & compatibility
- Capabilities
- database migrations · rls policies · edge functions · storage management · deployment
- Works with
- stripe · postgres
- Pricing
- Free
npx skills add https://github.com/insforge/agent-skills --skill insforge-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23.6k |
|---|---|
| repo stars | ★ 33 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | insforge/agent-skills ↗ |
How do you manage InsForge backend from the CLI?
devops
Who is it for?
Backend developers managing InsForge projects, database migrations, RLS policies, functions, storage, and production deployments
Skip if: Application code - use insforge SDK instead
When should I use this skill?
Managing backend infrastructure, creating migrations, deploying functions, or managing secrets
What you get
Applied migrations, RLS policies, branch merges, deployed services, configured secrets, and diagnostic log output.
- Applied migrations
- RLS policies
- Merged branch schemas
By the numbers
- 10+ command areas: db, config, storage, functions, payments, deployments, secrets, schedules
- Exit codes: 0 success, 2 not authenticated, 3 project not linked
Files
InsForge CLI
Use this skill whenever someone needs a backend, or when managing InsForge backend and cloud infrastructure with the InsForge CLI. For application code that calls InsForge from a frontend, backend, or edge function, use the insforge app-integration skill instead.
Core Rules
- Always run the CLI through
npx @insforge/cli <command>. Do not install or call a globalinsforgebinary. - If the project is already linked, use the current linked project. Run login, project creation, link, project discovery, organization listing, or cloud project commands only when connection setup is actually needed.
- Treat InsForge API keys as full-access admin keys. Keep them server-only and out of frontend/public env vars.
- Prefer CLI commands and documented project config over raw backend HTTP calls. If
config applyreports unsupported/skipped fields, surface that result instead of bypassing the CLI with direct API calls. - Use
--jsonwhen structured output or non-interactive value collection is needed. Use--yesfor confirmation prompts when the user has approved the action.
Global Options
| Flag | Use |
|---|---|
--json | Structured JSON output and skip value-collection prompts such as text/select prompts. Errors if any required value is missing. Combine with -y for destructive commands that also ask for Y/N confirmation. |
-y, --yes | Auto-accept Y/N confirmation prompts such as delete or overwrite prompts. Does not skip value-collection prompts; use --json for that. |
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error, including HTTP 400+ from function invoke |
| 2 | Not authenticated |
| 3 | Project not linked |
| 4 | Resource not found |
| 5 | Permission denied |
Environment Variables
| Variable | Use |
|---|---|
INSFORGE_ACCESS_TOKEN | Override stored access token |
INSFORGE_PROJECT_ID | Override linked project ID |
INSFORGE_EMAIL | Email for non-interactive login |
INSFORGE_PASSWORD | Password for non-interactive login |
Connection Setup
If a task needs project access and the connection state is unknown, start with npx @insforge/cli current. Use npx @insforge/cli whoami when the authenticated identity matters or when current reports that the CLI is not authenticated.
If not authenticated, run npx @insforge/cli login. If no project is linked, use npx @insforge/cli link for an existing project or npx @insforge/cli create when the user asked for a new backend. In workflows that are already prelinked or preconfigured, such as CI, local test projects, automation, or explicit user-provided project context, use that project context directly.
Command Routing
| Need | CLI area | Reference |
|---|---|---|
| Login, logout, current user | login, logout, whoami | references/login.md |
| Create/link/list/current project | create, link, list, current, metadata | references/create.md |
| Schema, SQL, RLS, triggers, indexes, imports, exports | db | references/database/* |
| Auth redirects, password policy, SMTP, storage size, realtime/schedule retention, subdomain config | config | references/config.md |
| Storage buckets and objects | storage | this file |
| Realtime backend setup | db migrations | references/realtime.md |
| Edge functions | functions | references/functions-deploy.md |
| AI/OpenRouter key setup | ai setup | this file |
| Stripe/Razorpay keys, catalog sync, webhooks | payments | references/payments/overview.md |
| Frontend deployments | deployments | references/deployments/deploy.md |
| Custom domains, Cloudflare Registrar, DNS sync, SSL verification | domains | references/deployments/domains.md |
| Backend containers/services | compute | references/compute-deploy.md |
| Secrets/env vars | secrets, deployment/compute env commands | this file |
| Scheduled jobs | schedules | references/schedules.md |
| Backend branches | branch | references/branch/overview.md, references/branch/merge.md, references/branch/reset.md |
| Logs and health checks | logs, diagnose | references/diagnostics.md |
| Built-in documentation lookup | docs | this file |
| PostHog setup | posthog setup | references/posthog.md |
Database Workflow
Use database references before writing migrations when the task involves non-trivial database work:
references/database/migrations.md- migration file creation and apply workflow.references/database/query.md- raw SQL execution and targeted inspection.references/database/access-control.md- RLS, grants, recursion-safe helper functions, ACLs, protected fields, and public projections.references/database/integrity.md- constraints, triggers, derived state, lifecycle guards, append-only history, and server-maintained fields.references/database/vector.md- pgvector extension, vector schema, distance operators, indexes, and vector search SQL/RPC patterns.references/database/export.md/references/database/import.md- schema or data import/export tasks.
Default pattern:
- Prefer
npx @insforge/cli db migrations new <name>plus a migration SQL file for schema, grants, indexes, triggers, functions, and RLS policy changes. - Apply migrations with
npx @insforge/cli db migrations up --all. - For new schema work, group related DDL into one migration when practical.
- Use targeted inspection when existing state is unknown or a command fails.
- Use
npx @insforge/cli db query <sql>for targeted inspection and small corrective row/data SQL only when a migration is not appropriate. - Use
npx @insforge/cli db rpc <fn> [--data <json>]to call database functions through the backend.
Public schema scope:
- For generic application database work, create and modify app-owned objects in the
publicschema. - Create, alter, drop, grant, revoke, index, trigger, function, view, and policy changes on
publicapplication objects. - Do not create custom schemas or write to InsForge-managed/system schemas such as
auth,storage,realtime,payments,graphql,extensions,pg_catalog,information_schema, orsystem, unless you are working on that specific feature module and its docs explicitly allow the operation. - It is allowed to reference built-in objects such as
auth.users(id)andauth.uid()from public tables or public RLS policies; do not modify those built-in objects. - Do not create users, seed business rows, or run application CRUD workflows unless the user request explicitly asks for data migration, repair, or test setup.
RLS and access control:
- Use
auth.uid()or an equivalent authenticated identity expression for user ownership checks. - Add both SQL privileges and RLS policies. Policies do not replace
GRANT. - Runtime roles have broad default DML privileges on
publictables so RLS can decide row access. If a table needs narrower operation or column access, explicitlyREVOKEthe broad privilege before granting the exact allowed operations or columns. - Include
WITH CHECKfor INSERT and UPDATE policies so writes cannot create rows the user should not own. - Prefer helper functions for cross-table RLS checks when direct policy joins can recurse through other RLS policies.
- Helper functions called from RLS policies that query RLS-enabled tables should be
SECURITY DEFINER. - Put RLS helper functions in
publicand schema-qualify references such aspublic.team_membersandauth.uid(). - For ACLs, protected owner/tenant/role fields, field-level update masks, sanitized public views, or recursion-sensitive policies, read
references/database/access-control.mdbefore writing migrations.
Integrity:
- For counters, balances, latest pointers, append-only history, state transitions, lifecycle guards, protected deletes, quota guards, leases, or trigger-maintained columns, read
references/database/integrity.mdbefore writing migrations.
Vector:
- For pgvector, vector search functions, score semantics, ANN indexes, hybrid ranking, RAG chunk retrieval, multi-vector search, or embedding version selection, read
references/database/vector.mdbefore writing migrations.
Project and Configuration
Project commands:
npx @insforge/cli create- create a new project. Use--jsonwith required flags for non-interactive agent runs. Seereferences/create.md.npx @insforge/cli link- link the current directory to an existing project.npx @insforge/cli current- show current linked project.npx @insforge/cli metadata --json- inspect backend metadata when discovery is needed.
Configuration:
- Use
npx @insforge/cli config export,config plan, andconfig applyfor supportedinsforge.tomlknobs. - TOML is for config values only. SQL belongs in
db migrations; function code belongs infunctions deploy; frontend code belongs indeployments deploy; compute code/images belong incompute deploy. - If
config applyreturnsskipped[], report the skipped items and required backend upgrade. Do not retry with raw HTTP.
Storage
npx @insforge/cli storage buckets- list buckets.npx @insforge/cli storage create-bucket <name> [--private]- create a bucket.npx @insforge/cli storage delete-bucket <name>- delete a bucket and all objects. Confirm destructive intent first.npx @insforge/cli storage list-objects <bucket> [--prefix] [--search] [--limit] [--sort]- inspect objects.npx @insforge/cli storage upload <file> --bucket <name> [--key <objectKey>]- upload an object.npx @insforge/cli storage download <objectKey> --bucket <name> [--output <path>]- download an object.
For storage access-control behavior implemented through Postgres policies, use the storage-specific product docs or feature guidance. Do not treat storage internals as generic public-schema database tables unless the referenced storage docs explicitly say to.
Realtime
Create channel patterns, app-table publish triggers, and channel/message RLS through migrations. See references/realtime.md.
Edge Functions
npx @insforge/cli functions list- list deployed functions.npx @insforge/cli functions code <slug>- view function source.npx @insforge/cli functions deploy <slug> --file <path>- deploy or update. Seereferences/functions-deploy.md.npx @insforge/cli functions invoke <slug> [--data <json>] [--method GET|POST]- invoke a function.npx @insforge/cli functions delete <slug>- delete a function. Confirm destructive intent first.
AI Gateway
npx @insforge/cli ai setupfetches the linked project's active OpenRouter key and writesOPENROUTER_API_KEYto a local server-side env file.- Keep
OPENROUTER_API_KEYserver-only. Never expose it asNEXT_PUBLIC_*,VITE_*,PUBLIC_*, orREACT_APP_*.
Payments
Use payments for Stripe/Razorpay backend setup and catalog sync. See references/payments/overview.md.
- Payments are provider-specific: use
payments stripe ...orpayments razorpay ...explicitly. - Configure provider keys with
payments <provider> config set; setting keys automatically syncs provider state when the key or account changes. - Check key/account/sync/webhook health with
payments <provider> status. - Run
payments <provider> syncto manually refresh or retry mirrored provider data. - Stripe uses Products/Prices and supports managed webhook registration; Razorpay uses Items/Plans/Orders and requires manual webhook setup in the Razorpay Dashboard.
- Prefer test mode while building. Use live mode only after explicit user approval.
- If the backend reports payments unavailable, ask the user/admin to enable or upgrade payments. Do not work around it by storing provider keys as generic secrets or embedding payment secret keys in app code.
- Load
references/payments/stripe.mdorreferences/payments/razorpay.mdbefore provider-specific setup.
Runtime checkout, subscriptions, customer portal flows, and app code belong in the insforge app-integration skill.
Deployments
Frontend deployments:
- Build locally first when the app has a build step.
- Ensure frontend runtime env vars are configured with the correct framework prefix before deployment.
- Use
npx @insforge/cli deployments deploy <dir>for frontend source directories. Do not deploy generated output directories unless the deployment reference explicitly calls for it. - See
references/deployments/deploy.md.
Custom domains:
- Use
npx @insforge/cli domains ...for custom domains, Cloudflare Registrar, DNS sync, and SSL verification. - See
references/deployments/domains.md.
Backend compute services:
- Use
npx @insforge/cli compute ...; do not manage InsForge compute services directly with the user's ownflyctlaccount. - Use source mode for a directory with a Dockerfile, or image mode with
--image <url>for a pre-built image. - Use
--env-fileor repeatable env-set/update commands for secrets instead of large inline JSON. - See
references/compute-deploy.md.
Secrets
npx @insforge/cli secrets list [--all]- list secret keys without values.npx @insforge/cli secrets get <key>- retrieve a secret value only when necessary.npx @insforge/cli secrets add <key> <value> [--reserved] [--expires <ISO date>]- create a secret.npx @insforge/cli secrets update <key> [--value] [--active] [--reserved] [--expires]- update a secret.npx @insforge/cli secrets delete <key>- soft-delete a secret. Confirm intent first.
Schedules
npx @insforge/cli schedules list/get/create/update/delete/logs.- Use standard 5-field cron for wall-clock schedules.
- Use pg_cron interval syntax such as
30 secondsfor sub-minute cadence. Six-field cron with seconds is not supported. - Headers can reference InsForge secrets with
${{secrets.KEY_NAME}}. - See
references/schedules.mdfor cron formats, secret header references, examples, common mistakes, and the recommended setup workflow.
Branching
Use backend branches to test risky schema, RLS, auth, or function changes before applying them to production. See references/branch/overview.md.
Common commands:
npx @insforge/cli branch create <name> [--mode full|schema-only] [--no-switch]npx @insforge/cli branch listnpx @insforge/cli branch switch <name>or--parentnpx @insforge/cli branch merge <name> [--dry-run] [--save-sql <path>]npx @insforge/cli branch reset <name>npx @insforge/cli branch delete <name>
Branching requires a backend version that supports it. If unavailable, report the backend version limitation instead of inventing a workaround.
Diagnostics and Logs
npx @insforge/cli diagnose- full health report.npx @insforge/cli diagnose --ai "<issue description>"- ask the InsForge debug agent to diagnose a concrete backend issue.npx @insforge/cli diagnose metrics [--range 1h|6h|24h|7d]- EC2 metrics.npx @insforge/cli diagnose advisor [--severity critical|warning|info] [--category security|performance|health]- advisor issues.npx @insforge/cli diagnose db [--check <checks>]- database health checks.npx @insforge/cli diagnose logs [--source <name>] [--limit <n>]- aggregate error logs.npx @insforge/cli logs <source> [--limit <n>]- source-specific backend logs.
Typical log sources include function.logs, function-deploy.logs, postgres.logs, postgrest.logs, and insforge.logs. See references/diagnostics.md for common debugging scenarios and source selection.
Documentation
npx @insforge/cli docs- list documentation topics.npx @insforge/cli docs instructions- setup guide.npx @insforge/cli docs <feature> <language>- feature docs fordb,storage,functions,auth,ai, orrealtimeintypescript,swift,kotlin, orrest-api.
For application code with InsForge or @insforge/sdk, use the insforge app-integration skill and use docs only as official feature reference.
PostHog
npx @insforge/cli posthog setupensures the dashboard has a PostHog connection, then prints the official PostHog wizard command plus the connected project's publicphc_API key and host.- ⚠️
posthog setupalone does NOT instrument the app: no env vars, no SDK, no events until the wizard step happens. The wizard is interactive and may open a browser; ask the user to run it in their real terminal, or instrument manually using the printedphc_key/host (PostHog's public client key, safe in frontend env vars). - Cloud only: self-hosted backends don't expose the integration. Do not substitute a
phc_key from a separate PostHog account into app env vars — the Analytics page reads from the server-side connection that onlyposthog setuppopulates; use the key it prints.
Non-Interactive CI/CD
Use env vars and JSON mode for automated contexts:
INSFORGE_EMAIL=$EMAIL INSFORGE_PASSWORD=$PASSWORD npx @insforge/cli login --email -y
npx @insforge/cli link --project-id $PROJECT_ID --org-id $ORG_ID -y
npx @insforge/cli db query "SELECT 1 AS ok" --jsonProject Configuration File
After create or link, .insforge/project.json contains the linked project ID, app key, region, API key, and backend URL.
- Never commit
.insforge/project.jsonor share it publicly. - Do not edit it manually. Use
npx @insforge/cli linkor branch commands to switch projects.
Auth Backend Configuration
Use migrations for database-side auth lifecycle hooks. The common case is creating an app-owned profile row whenever a new InsForge user is created.
auth.users Fields Agents Commonly Need
Do not rely on a full auth.users schema dump in skills. For common app hooks, these fields are safe to assume:
| Field | Use |
|---|---|
id | User UUID; reference it with auth.users(id) |
email | User email |
profile | JSONB profile metadata from sign-up/OAuth, such as name and avatar_url |
InsForge stores profile metadata in auth.users.profile JSONB. In triggers, read common values with NEW.profile->>'name' and NEW.profile->>'avatar_url'.
Create a Profile on Sign Up
CREATE TABLE IF NOT EXISTS public.profiles (
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
display_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY profiles_owner_select ON public.profiles
FOR SELECT TO authenticated
USING (user_id = (SELECT auth.uid()));
CREATE POLICY profiles_owner_update ON public.profiles
FOR UPDATE TO authenticated
USING (user_id = (SELECT auth.uid()))
WITH CHECK (user_id = (SELECT auth.uid()));
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (user_id, display_name, avatar_url)
VALUES (
NEW.id,
NEW.profile->>'name',
NEW.profile->>'avatar_url'
)
ON CONFLICT (user_id) DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();Create the trigger function in public, then attach the trigger to auth.users. To remove the hook later, drop the function with CASCADE:
DROP FUNCTION IF EXISTS public.handle_new_user() CASCADE;Keep app data in app-owned tables such as public.profiles; do not add custom columns to auth.users.
npx @insforge/cli branch merge
Merge a branch's schema, config, and data-level changes back into the parent.
Syntax
npx @insforge/cli branch merge <name> [options]Options
| Option | Default | Description |
|---|---|---|
--dry-run | off | Compute the diff and print rendered SQL; do not apply. |
-y, --yes | off | Skip the "are you sure" confirmation when applying. |
--save-sql <path> | — | Write the rendered SQL preview to a file (works with or without --dry-run). |
Inherits --json and --api-url.
Always run --dry-run first
The dry run prints a migration-style SQL preview, organized by section:
-- Generated 2026-04-29T12:00:00Z
BEGIN;
-- ===== MIGRATION =====
-- [MIGRATION] migration system.060 (add)
-- Migration 060: add_visibility_to_posts
ALTER TABLE public.posts ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private';
INSERT INTO "system"."custom_migrations" ("version", "name", "statements", "created_at") VALUES (...)
ON CONFLICT ("version") DO UPDATE SET ...;
-- ===== DATA =====
-- [DATA] config_row email.templates (modify)
INSERT INTO "email"."templates" ("template_type", "subject", ...) VALUES (...)
ON CONFLICT ("template_type") DO UPDATE SET "subject" = EXCLUDED."subject", ...;
COMMIT;Read it. If anything looks wrong, do not run merge without --dry-run.
Merge order (matters)
The cloud-backend orders the SQL such that:
1. Migrations (DDL via system.custom_migrations.statements[]) run first, so any newly added tables/columns exist when data lands. 2. Config rows (UPSERTs into the 13 mergeable matrix tables) and edge functions (UPSERTs into functions.definitions) run second.
The whole script is wrapped in BEGIN; … COMMIT; — any failure rolls the parent's PG back to the pre-merge state, and branch_state flips from merging back to ready.
Conflicts
If the cloud-backend reports branch.merge_conflict (HTTP 409), the preview SQL is prefixed with:
-- ⚠️ MERGE BLOCKED: 1 conflict(s) detected. Resolve before applying.
--
-- [CONFLICT] table public.users
-- parent_t0_hash: <hash>
-- parent_now_hash: <different hash>
-- branch_now_hash: <different hash>
-- hint: Both parent and branch modified this object after branch creation. Resolve manually.The CLI exits with code 2 (distinct from the generic error exit 1).
Resolution steps
1. Inspect parent's current state and branch's current state for the conflicted object (e.g. npx @insforge/cli db tables / db policies). 2. Decide which version to keep:
- Keep parent: revert the branch's change (drop the column on branch, etc.) and run
branch merge --dry-runagain. - Keep branch: forcibly apply the branch's version on parent (manually), then merge — auto-merge will see no conflict because parent_now will match branch_now.
- Hand-merge: write a manual migration that combines both intents, apply it on the branch, then merge.
3. Re-run branch merge <name> --dry-run to confirm zero conflicts, then run without --dry-run.
What gets auto-applied
The full v1 matrix, by (diff type, action). The user-schema DDL paths (table / policy / function) replay introspected SQL — you don't have to wrap every change in a system.custom_migrations entry, though doing so is still the safest option for complex changes.
| Type | add | modify | drop |
|---|---|---|---|
config_row (13 mergeable tables) | ✅ UPSERT keyed on the matrix conflict column, respecting excludeColumns / excludeKeys (e.g. OAuth client_secret is filtered out) | ✅ same as add — UPSERT replaces the row | ❌ skip — never auto-DELETE from parent. Drop it manually if intended. |
edge_function (functions.definitions) | ✅ UPSERT keyed on slug | ✅ same as add | ❌ skip — delete the function on parent via dashboard or cli functions delete |
migration (system.custom_migrations) | ✅ replays statements[] verbatim, then UPSERTs the migration row | — | ❌ skip — append-only by design |
table (user schemas only) | ✅ replays the introspected CREATE TABLE IF NOT EXISTS … (columns + inline constraints) plus CREATE INDEX for any captured indexes | ❌ skip — column-level diff isn't implemented in v1. Workaround: write an ALTER TABLE in a system.custom_migrations entry on the branch — it lands via the migration:add path. | ✅ DROP TABLE IF EXISTS schema.table CASCADE |
policy (user-defined RLS) | ✅ DROP POLICY IF EXISTS … ; CREATE POLICY … (the leading drop keeps it idempotent against the OSS create_policies_on_table_create event trigger) | ✅ same as add — rebuilds to the branch's current spec | ✅ DROP POLICY IF EXISTS … |
function (user-defined PG functions) | ✅ replays pg_get_functiondef (CREATE OR REPLACE FUNCTION …) — idempotent for both add and modify | ✅ same as add | ✅ DROP FUNCTION IF EXISTS schema.fn(arg-types) — uses pg_get_function_identity_arguments so overloads resolve precisely |
Row data on user tables is never auto-merged — branches are not the source of truth for parent's user data. If you seeded rows into public.* tables on the branch and want them on parent, copy them manually after the merge (e.g. via db query or a one-off db import).
All auto-apply SQL is idempotent (IF EXISTS / CREATE OR REPLACE / UPSERT). This matters because OSS event triggers like create_policies_on_table_create will rebuild policies after the table:add step lands — the subsequent policy:add step then overwrites them with the branch's exact spec. You should not see drift after merge, but if you do, re-running merge is safe.
Schemas covered by the DDL paths: public and any user-defined schema. System schemas (auth, storage, functions, email, ai, realtime, schedules, system, deployments, cron) are gated by the mergeable matrix — DDL on them propagates only via system.custom_migrations append, never via table / policy / function diffs.
Skipped items are recorded in the unsupported line on the apply response.
After the merge
The branch enters merged state — dormant, not destroyed. To layer further changes onto the same branch slot, `branch reset` rewinds it to T0 and flips state back to ready.
The merge does not redeploy code. Re-run functions deploy, deployments deploy, and compute update for anything outside the database that depends on the new schema.
Example
$ npx @insforge/cli branch merge feat-rls-fix --dry-run --save-sql /tmp/diff.sql
BEGIN;
…
COMMIT;
2 added, 1 modified, 0 conflict(s).
$ cat /tmp/diff.sql # review the SQL with a human eye
$ npx @insforge/cli branch merge feat-rls-fix
2 added, 1 modified, 0 conflict(s).
? Apply this merge to parent project 'my-app'? › yes
✓ Merged. Branch 'feat-rls-fix' is now in 'merged' state.
⚠ Reminder: redeploy edge functions, website, and compute as needed.See also
- branch overview — lifecycle commands and decision guide
- branch reset — rewinding a branch to T0
Backend Branches — npx @insforge/cli branch
A branch is a full child of the parent project: own EC2, own PostgreSQL, own storage namespace. It shares the parent's JWT_SECRET (same users authenticate) but gets fresh API_KEY / ANON_KEY. Use it to test schema, RLS, auth, or function changes in isolation before merging back to parent.
Branching is not free — each branch consumes an EC2 instance. Use it when isolation pays off.
When to use a branch
Strong signals — branch first:
- Destructive DDL on existing tables (
DROP TABLE,DROP COLUMN,ALTER COLUMN TYPE).git revertdoesn't restore lost data. - New or modified RLS policies on user-data tables. RLS bugs are silent — prod users lock out or get unintended access.
- Auth provider config changes (OAuth providers, redirect URIs, SMTP). Bricks prod login if wrong.
- Multi-step refactors touching >3 tables or >1 schema.
Moderate signals — branch if convenient:
- Adding a new table or column (additive).
- Email templates, AI gateway config, cron schedule changes.
Skip the branch:
- Row-data-only changes (insert/update). Branching is about schema, not data.
- Client-side fixes that don't touch the backend.
- Edge-function logic-only changes covered by unit tests.
- Anything
git reverthandles faster.
Mode selection
| Mode | When |
|---|---|
full (default) | Need realistic data — RLS testing with real rows, query plan tuning, large-table migrations. |
schema-only | Synthetic seed rows are enough. Faster to create. User-data tables (auth.users, storage.objects, …) start empty. |
Mode is fixed at create time — branch reset uses the original dump. Need a different mode → delete + recreate.
Lifecycle commands
branch create <name> [--mode full|schema-only] [--no-switch]
Creates a branch from the linked parent and auto-switches the directory's context to it (unless --no-switch). Provisioning takes 30–120 s for small DBs, longer for large.
<name>: 1–64 chars, [a-zA-Z0-9-], must start with letter/digit, unique per parent.
After creation:
1. Re-source your dev server's `.env` — INSFORGE_URL / INSFORGE_ANON_KEY change with the switch. 2. Deploy code that lives outside the database. pg_dump copies functions.definitions rows but not the Deno Subhosting bundles, Vercel frontends, or Fly.io compute services — the branch's runtime starts empty. Run functions deploy <slug>, deployments deploy, and compute deploy for anything you need on the branch (compute deploy, not compute update — there's no service id to update yet). Symptom if you skip this: function invocations fail with getaddrinfo ENOTFOUND deno or Deployment not found.
branch list
Lists active branches of the parent (or, when on a branch, that branch's siblings). The leading column shows * for the branch the directory is currently switched onto.
| State | Meaning |
|---|---|
creating | Provisioning EC2 + restoring pg_dump (30–120 s). |
ready | Usable — can be switched, modified, merged, or reset. |
merging | Merge in progress (usually < 30 s). |
merged | Last merge succeeded. Dormant — branch reset rewinds to T0 and flips back to ready so the same slot can be reused. |
resetting | branch reset is restoring the T0 dump in place. |
deleted | Soft-delete tombstone (filtered from list). |
branch switch <name> / --parent
Repoints .insforge/project.json at the branch (or back at the original parent). Refuses if the target branch isn't ready.
Critical: the dev server's.envis not updated byswitch. The SDK readsINSFORGE_URL/INSFORGE_ANON_KEYfrom.env, so without re-sourcing, the SDK silently keeps hitting the previous backend. This is the #1 source of "I switched but my changes aren't showing up."
Also: each backend has its own function / frontend / compute runtime. Switching points the SDK at a different EC2 whose Deno Subhosting, Vercel, and Fly.io state are independent. If you've never deployed your code on the target (e.g. first switch to a freshly-created branch), deploy it withfunctions deploy,deployments deploy, andcompute deploy— otherwise calls land on an empty runtime and fail withgetaddrinfo ENOTFOUND deno/Deployment not found.
The first hop off the parent backs up .insforge/project.json to .insforge/project.parent.json. Subsequent branch ↔ branch switches don't touch the backup — --parent always returns to the original.
branch delete <name> [-y]
Deletes a branch and reclaims its EC2. Auto-switch --parent if the directory is currently on the deleted branch. Irreversible — branch data is lost. Already-merged branches: deletion still works (the merge has already landed on parent).
Reset vs. delete + recreate
| Want to… | Reach for |
|---|---|
Rerun the experiment from a clean T0, keep the same API_KEY / URL so dev-server config is unchanged | branch reset |
A different --mode (mode is fixed at create time) | delete + create |
A fresh appkey / API key so callers can't talk to the old branch | delete + create |
Re-merge a branch already in merged with new changes layered on T0 | branch reset (re-opens the slot), make new changes, branch merge again |
See branch reset for what reset does and does not touch.
Failure modes
| Error | Meaning | Fix |
|---|---|---|
branch.quota_exceeded | Per-org cap (3 parents) or per-parent cap (2 branches) reached | Delete an old branch first |
branch.parent_not_branchable | Parent is itself a branch / not active / pre-2.x | Use a top-level 2.x project |
branch.name_conflict | Branch name already exists on this parent | Pick a different name |
branch.not_found | No branch with that name on the parent | Check branch list |
branch.busy | Branch is creating / merging / resetting | Wait for the in-flight op |
branch.not_ready | Branch isn't in ready state for this op | Wait or check state |
Limits
- Per-org: max 3 parent projects with active branches (configurable).
- Per-parent: max 2 active branches (configurable).
- Branches do not nest (no branch-of-a-branch).
- Branches do not auto-resume when the parent resumes — resume manually.
- Branches are deleted (cascade) when the parent project is deleted.
See also
- branch merge — merging a branch back to parent (dry-run, conflict resolution, what gets applied)
- branch reset — rewinding a branch to T0 (recovery / re-merge)
npx @insforge/cli branch reset
Reset a branch's database back to T0 — the parent's snapshot at the moment the branch was created. Use when the branch is in a bad state and you'd rather start over than untangle it. Cheaper than branch delete + branch create: same EC2, same appkey, same API_KEY / ANON_KEY — only the database content is rewound.
Syntax
npx @insforge/cli branch reset <name> [-y]| Option | Description |
|---|---|
-y, --yes | Skip the confirmation prompt. |
Inherits the global --json and --api-url flags.
What this does
1. Resolves <name> to a branch via the parent's branch list (works whether the directory is on the parent or on a sibling branch). 2. Rejects the call unless branch_state is ready or merged. creating / merging / resetting / deleted all return 409 (BRANCH_BUSY or BRANCH_NOT_READY). 3. Confirms (unless --yes or --json). 4. POST /projects/v1/branches/{branchId}/reset — backend transitions branch_state to resetting and enqueues pg_restore against branch_metadata.parent_t0.source_backup_s3_key (the dump captured at branch creation). 5. For schema-only branches, the backend re-runs schema-only-truncate.sql after the restore — same finalize chain as branch create --mode schema-only. 6. CLI polls GET /branches/{branchId} every 3 s for up to 5 min until branch_state returns to a terminal state.
Final state
Reset always lands at `ready`, even if the branch entered reset from merged. A merged branch reset to T0 becomes usable again — you can edit it and merge it a second time without recreating the EC2.
If the SSM restore fails halfway, the backend rolls branch_state back to the entry state (ready or merged). The CLI surfaces this via the polled state. However, pg_restore is destructive once it starts — the database may be in an indeterminate state between T0 and pre-reset. If reset fails, retry it, or fall back to a project backup (paid plans) instead of trying to recover the in-flight state.
What reset does NOT touch
- The branch's EC2 instance — same machine, same
appkey, same URLs. API_KEY/ANON_KEY/JWT_SECRET— unchanged. SDK /.envkeep working without re-sourcing.- The parent project — completely untouched. Reset is local to the branch.
- Edge functions deployed to the branch's
functions.definitionstable — these are part of the DB and are rolled back to T0 along with everything else. Redeploy any branch-specific functions after reset if you need them again. - Vercel deployments and Fly.io compute services — these live outside the database, so reset won't roll them back. Redeploy manually if their behavior depends on the schema you just rewound.
branch_metadata.parent_t0andbranch_created_at— not modified. T0 is the same anchor as before.
Quota
Reset does not count against the per-org or per-parent branch quota — quota is computed from the active branch count, and reset doesn't change it.
Concurrency
Same BUSY set as merge: only one of creating / merging / resetting can be in flight per branch. The backend enforces this; the CLI surfaces 409s as BRANCH_BUSY.
Failure modes
| Error | Meaning | Fix |
|---|---|---|
branch.not_found | No branch with that name on the parent | Check branch list |
branch.not_ready | Branch is in creating / merging / resetting / deleted | Wait for the in-flight op, then retry |
| Reset polled out at 5 min | SSM job is still running on a large DB | Re-run branch list periodically; the backend will eventually settle the state |
Branch lands at entry state instead of ready | The async restore rolled back. PG content is indeterminate | Retry reset, or restore from a project backup |
See branch overview for the reset-vs-delete decision matrix.
Example
$ npx @insforge/cli branch reset feat-rls-fix
? Reset branch 'feat-rls-fix' back to T0? This wipes all schema/data/policy/function/migration changes made on the branch since creation. › yes
✓ Reset enqueued for branch 'feat-rls-fix'. Restoring T0…
state: resetting…
✓ Branch 'feat-rls-fix' is back to T0 and ready.
⚠ Reminder: edge functions, website, and compute aren’t touched by reset; redeploy if needed.Reset works the same on a merged branch — it lands at ready and the slot is reusable for another round of changes.
See also
- branch overview — lifecycle commands and decision guide
- branch merge — merging a branch back to parent
npx @insforge/cli compute deploy — deploy a backend container
🔒 Private preview. Compute services are not yet generally available.
Access is gated per-project; the API, CLI flags, error codes, and quotas
may change between releases. To request access, raise quotas, configure a
private registry, or report issues, contact the InsForge team
(support@insforge.dev or your shared Slack channel).
🔧 DO NOT call `flyctl` directly to manage InsForge compute services.
InsForge runs containers on Fly.io under the hood, but the Fly account, org,
IPs, and machine ownership all live on the InsForge cloud. Using flyctlwith your own credentials will land in the wrong Fly org and fail with
unauthorized. Usenpx @insforge/cli compute …instead.
Deploy a backend service. Two modes: 1. Source mode (compute deploy [dir]): you have a Dockerfile. CLI shells out to flyctl deploy --remote-only --build-only using a short-lived per-app deploy token minted by InsForge cloud. Build runs on Fly's remote builder; image is pushed to registry.fly.io. Cloud then launches the machine. No local Docker daemon needed — only flyctl on PATH. 2. Image mode (compute deploy --image <url>): deploy a pre-built image from any registry. Nothing needed locally beyond the InsForge CLI.
Looking to deploy a frontend (static site / SPA / Next.js to Vercel)? Use
npx @insforge/cli deployments deploy instead — seedeployments/deploy.md.
Two modes
| Mode | Command | When to use | Local tooling |
|---|---|---|---|
| Source | compute deploy ./my-app --name my-api | You have a Dockerfile and want one command. Build runs on Fly's remote builder via flyctl. | `flyctl` on PATH (no Docker needed) |
| Image | compute deploy --image <url> --name my-api | You already have a built image (CI pipeline, public image, custom registry). | None |
Both deploy to the same Fly.io infrastructure with the same options (--port, --cpu, --memory, --region, --env).
Anti-pattern: `flyctl deploy` directly from your laptop with your own credentials. Returns 401 — the Fly account is InsForge's, not yours. The CLI invokes flyctl for you with the cloud-minted per-app token, which is the only token that works.
Syntax
# Source mode — flyctl remote build + push, then cloud launches the machine.
# Requires `flyctl` on PATH (curl -L https://fly.io/install.sh | sh). NO Docker daemon needed.
# Cloud mints a 20-min per-app token attenuated to one app + builder/wg with `else: deny`.
npx @insforge/cli compute deploy <dir> --name <name> [options]
# Image mode — deploy pre-built image (nothing needed locally).
npx @insforge/cli compute deploy --image <url> --name <name> [options]Options
| Option | Description | Default |
|---|---|---|
--name <name> | Service name (DNS-safe: lowercase, numbers, dashes) | required |
[dir] (positional) | Source directory containing a Dockerfile (source mode) | — |
--image <url> | Docker image URL (image mode) | — |
--port <port> | Container internal port | 8080 |
--cpu <tier> | CPU tier in Fly.io standard format <kind>-<N>x (see CPU tier section) | shared-1x |
--memory <mb> | Memory in MB (any positive integer; Fly enforces per-tier bounds) | 512 |
--region <region> | Fly.io region | iad |
--env <json> | Env vars as JSON object. Mutually exclusive with --env-file. | none |
--env-file <path> | Standard .env file (KEY=VALUE per line; # comments, blank lines, quoted values supported). Mutually exclusive with --env. | none |
Exactly one of [dir] or --image must be provided.
Quick examples
# Source mode — your project, your Dockerfile, flyctl on PATH (no Docker needed)
npx @insforge/cli compute deploy . --name my-api --port 8000
# Off-the-shelf image
npx @insforge/cli compute deploy --image nginx:alpine --name proxy --port 80
# Pre-built image from GHCR
npx @insforge/cli compute deploy \
--image ghcr.io/your-org/your-app:v1 \
--name my-api \
--port 8000 \
--cpu performance-1x \
--memory 2048 \
--env '{"OPENAI_API_KEY": "sk-..."}'
# Bigger machine (8 cores + 4 GB RAM)
npx @insforge/cli compute deploy ./worker \
--name batch \
--port 8080 \
--cpu performance-8x --memory 4096
# Env vars from a .env file (preferred for >1 secret)
npx @insforge/cli compute deploy \
--image ghcr.io/your-org/your-app:v1 \
--name my-api \
--port 8000 \
--env-file ./.env.productionRotating env vars after deploy
The GET path never returns env values (encrypted at rest, no decrypt endpoint). To rotate one secret without wiping the others, use partial-merge flags on compute update instead of --env:
# Partial merge — keeps untouched keys intact (repeatable flags)
npx @insforge/cli compute update <id> \
--env-set DATABASE_URL=postgres://new-host \
--env-set API_KEY=sk-... \
--env-unset OLD_DEBUG_TOKEN
# Wholesale replace — clears anything not in the JSON. Mutually exclusive
# with --env-set / --env-unset.
npx @insforge/cli compute update <id> --env '{"NODE_ENV":"production","DATABASE_URL":"..."}'Source mode — worked example
# Project layout:
$ ls
Dockerfile app.py requirements.txt
# Deploy:
$ npx @insforge/cli compute deploy . --name my-bot --port 8080
✓ Detected Dockerfile at /path/to/Dockerfile
✓ Creating service "my-bot"...
✓ Created Fly app my-bot-projAbc
✓ Requesting deploy token...
✓ Building & pushing on Fly remote builder...
[flyctl streams build logs here]
✓ Launching machine...
✓ Service "my-bot" deployed [running]
Endpoint: https://my-bot-projAbc.fly.dev
Image: registry.fly.io/my-bot-projAbc:cli-1714003200000 (built remotely; no local image to clean up)What happens behind the scenes: 1. CLI looks up the service by --name. If missing, calls the cloud to provision a Fly app shell (no machine yet) and gets back the flyAppId. 2. CLI requests a per-app deploy token from the cloud — a Fly macaroon attenuated with IfPresent { ifs: [Apps[<thisAppOnly>: rwcdC], FeatureSet[builder, wg]], else: deny } and a 20-min ValidityWindow. The org-wide Fly token never leaves InsForge's servers. 3. CLI shells out to flyctl deploy --remote-only --build-only --app <flyAppId> --image-label cli-<ts> with the token exported as FLY_API_TOKEN. flyctl ships the build context to Fly's remote builder, the build runs there, and the resulting image is pushed straight to registry.fly.io/<app>:cli-<ts>. Nothing built or pushed from your laptop — and no Docker daemon needed. 4. CLI sends PATCH /api/compute/services/<id> with imageUrl=registry.fly.io/<app>:cli-<ts>. Cloud calls Fly Machines API to launch (or restart with the new image) and returns the public URL.
When to use source mode vs image mode
- Source mode: rapid iteration on a single project, Dockerfile in repo,
flyctlon PATH. No need for Docker Desktop or a local daemon. - Image mode: no
flyctlon the machine running the CLI (e.g. constrained CI runners), pipelines that push their own images, off-the-shelf images likenginx:alpine, or multiple deploy targets sharing one image.
If you don't have a Dockerfile yet
Ask your AI agent to generate one for your stack:
- Node app → typically
FROM node:20-alpine,npm ci,CMD node index.js - Python app →
FROM python:3.12-alpine,pip install -r requirements.txt,CMD python app.py - Go binary → multi-stage build with
FROM golang:1.22 AS buildthenFROM alpine:3.20
The InsForge skill knows these patterns; ask the agent and it'll write one.
Producing an image yourself (for image mode)
If you want to build images in CI and deploy via --image instead:
docker build -t ghcr.io/<your-gh-username>/<app-name>:v1 .
echo $GITHUB_TOKEN | docker login ghcr.io -u <your-gh-username> --password-stdin
docker push ghcr.io/<your-gh-username>/<app-name>:v1
npx @insforge/cli compute deploy \
--image ghcr.io/<your-gh-username>/<app-name>:v1 \
--name <app-name> \
--port <port>Any OCI registry works (GHCR, Docker Hub, etc.) as long as the image is publicly pullable. Private registries require per-project credential setup — contact support.
CPU Tier (Fly.io standard format)
--cpu accepts any well-formed Fly.io machine size in the format `<kind>-<N>x` where:
<kind>issharedorperformance<N>is the vCPU count
InsForge does not maintain a hardcoded allow-list — Fly.io is the source of truth for which sizes actually exist. If you pass an unsupported combination (e.g. performance-32x), Fly returns a clean validation error at machine-create time.
Common standard tiers (current as of writing):
| Tier | Kind | vCPU | Typical RAM range |
|---|---|---|---|
shared-1x (default) | shared | 1 | 256 MB – 2 GB |
shared-2x | shared | 2 | 512 MB – 4 GB |
shared-4x | shared | 4 | 1 GB – 8 GB |
shared-8x | shared | 8 | 2 GB – 16 GB |
performance-1x | dedicated | 1 | 2 GB – 8 GB |
performance-2x | dedicated | 2 | 4 GB – 16 GB |
performance-4x | dedicated | 4 | 8 GB – 32 GB |
performance-8x | dedicated | 8 | 16 GB – 64 GB |
performance-16x | dedicated | 16 | 32 GB – 128 GB |
Authoritative current list and pricing: <https://fly.io/docs/about/pricing/#started-machines>.
Common picks
| Use case | Recommended --cpu --memory |
|---|---|
| Static site / proxy | shared-1x 256 |
| Small Node/Python API | shared-1x 512 |
| Mid API with caching | shared-2x 1024 |
| API needing 4 GB RAM | shared-2x 4096 or shared-4x 4096 |
| 8 cores + 4 GB (CPU-heavy short jobs) | performance-8x 4096 |
| ML inference (CPU) | performance-4x 8192 |
| Heavy data processing | performance-8x 16384 |
What happens internally
CLI → OSS instance → InsForge cloud backend → Fly.io. The cloud: 1. Records the service in its compute_services table 2. Creates a Fly.io app named <name>-<projectId> 3. Allocates IPv4 + IPv6 addresses 4. Launches a Fly machine pulling the image you specified 5. Returns the public endpoint URL
Total time: typically ~5 seconds (Fly pulls the image and boots the machine).
Output
Text mode:
✓ Service "my-api" deployed [running]
Endpoint: https://my-api-projID.fly.devJSON mode (--json):
{
"id": "uuid",
"name": "my-api",
"imageUrl": "ghcr.io/you/app:v1",
"port": 80,
"cpu": "shared-1x",
"memory": 256,
"region": "iad",
"status": "running",
"endpointUrl": "https://my-api-projID.fly.dev",
"flyAppId": "my-api-projID",
"flyMachineId": "abc123"
}Common errors
| Error | Cause | Solution |
|---|---|---|
COMPUTE_SERVICE_ALREADY_EXISTS | Duplicate name in project | Choose a different name or delete the existing service |
COMPUTE_QUOTA_EXCEEDED | At per-project quota (5 active services) | Delete unused services with compute delete <id>. If the dashboard shows fewer services than the error implies, contact support to clear orphans. |
COMPUTE_INVALID_CPU_TIER | --cpu doesn't match <kind>-<N>x | Use the format above, e.g. performance-2x |
COMPUTE_IMAGE_NOT_AVAILABLE | Fly registry alias propagation race exhausted retries (rare) | Re-run the deploy. The cloud silently retries this race 4 times with backoff [2s, 4s, 8s]; this error only surfaces if all retries failed. |
COMPUTE_FLY_API_ERROR | Generic Fly 4xx (bad config, region mismatch, etc.) | Read the structured error message — it's the upstream Fly response and usually points at the specific field. |
flyctl is required for source-mode deploy | flyctl isn't installed or not on PATH | Install: `curl -L https://fly.io/install.sh \ |
flyctl deploy ... unauthorized | Per-app deploy token expired (20-min TTL) | Re-run compute deploy — the CLI mints a fresh token per invocation |
flyctl deploy --build-only failed | Build error in your Dockerfile | Check the build output above (streamed from Fly's remote builder); fix the Dockerfile and retry |
Image pull error (image mode) | Registry private without InsForge having creds | Push to a public image, or contact support to configure private registry creds |
Unauthorized from registry (image mode) | Image is private and InsForge cloud doesn't have credentials | Make the image public, or use a public registry |
FAQ
Q: Why does source mode need `flyctl` if it doesn't need Docker? A: The CLI shells out to flyctl deploy --remote-only --build-only for the build step — flyctl knows how to ship a build context to Fly's remote builder, stream logs back, and push the result. Image mode skips that entirely (it's just an HTTP call telling the cloud which image URL to pull), so it needs nothing locally.
Q: Where does the deploy token come from? Can a stolen token attack other tenants? A: The cloud holds the org-wide Fly token; it never leaves InsForge servers. Per compute deploy invocation it mints a fresh app-scoped macaroon with IfPresent { ifs: [Apps[<oneApp>: rwcdC], FeatureSet[builder, wg]], else: deny } + 20-min ValidityWindow. If exfiltrated within those 20 minutes, the token can deploy to that one app and use the org's remote builder to do so — but cannot read or mutate any other app, list org-level inventory, mint new tokens, or persist beyond TTL. Verified by the live e2e suite which probes /v1/orgs/<slug>/machines, /v1/apps?org_slug=, and /v1/orgs/<slug>/volumes and asserts each returns 4xx.
Q: Can I use a private image from my own registry? A: Public images (e.g. Docker Hub public, GHCR public) work out of the box. Private registry support requires per-project credential configuration; contact support to set this up.
Q: How do I update a running service to a new image? A: Use compute update <service-id> --image <new-image-url>. The machine is restarted with the new image; ~5s downtime.
Q: What happens to my service if Fly.io has an outage? A: It's down. InsForge runs your containers on Fly's infrastructure — Fly's uptime is your uptime. For HA, you'd typically deploy multiple services in different regions (future feature).
Q: Why is the first request after idle slow? A: v1 services scale to zero when idle and wake on the next request (~1s cold start on shared-1x). No flag to disable in v1; contact support if you need always-on.
Q: I see `MANIFEST_UNKNOWN` in a stack trace. What is it? A: After flyctl pushes your image, Fly asynchronously aliases the digest from the builder's namespace to your app's namespace. Until that propagates (usually < 8 s) the Machines API returns 400 MANIFEST_UNKNOWN even though the digest is correct. The InsForge cloud silently retries 4 times with backoff [2s, 4s, 8s], so you almost never see it. If retries exhaust, you get a structured COMPUTE_IMAGE_NOT_AVAILABLE 400 with nextActions telling you to re-run — re-runs are idempotent and typically succeed instantly because the alias has had time to propagate.
Notes
- The user never needs to handle a Fly token. The InsForge cloud holds the org token; per deploy it mints an app-scoped, attenuated token (~20 min,
else: deny) and the CLI exports it asFLY_API_TOKENonly for the duration of the flyctl subprocess. - Source mode requires
flyctlon PATH but no local Docker daemon (build runs on Fly's remote builder). Image mode requires neither. - The machine starts immediately on first deploy. Subsequent deploys to the same
--nameupdate the existing machine in place. Usecompute stopto pause without destroying. - Env vars are encrypted at rest. See Rotating env vars after deploy for partial-merge usage on running services.
compute deleteis permanent: Fly app + image are destroyed and the registry GCs the image shortly after. The audit log captures the full config (encrypted env blob included) on delete for after-the-fact reconstruction. Dashboard adds a type-to-confirm gate; the CLI does not.
npx @insforge/cli config
Deep reference for config export | plan | apply. The SKILL.md Configuration section has the principles and rules; this file has output shapes and the error table.
Scope today: auth redirects and verification flags, password policy, SMTP, storage upload size, realtime/schedule retention, and cloud deployment subdomain. TOML does not manage external provider resources such as OAuth apps, storage bucket lifecycle, realtime channels, deployment env vars, functions, or secrets.
Commands
npx @insforge/cli config export [--out insforge.toml] [--force]
npx @insforge/cli config plan [--file insforge.toml]
npx @insforge/cli config apply [--file insforge.toml] [--dry-run] [--auto-approve]File location
insforge.toml lives at the project root, alongside package.json and .insforge/project.json. Safe to commit to git.
Output shapes (--json mode)
config export:
{
"written": "/abs/path/to/insforge.toml",
"config": {
"auth": {
"allowed_redirect_urls": ["https://app.com"],
"require_email_verification": true,
"verify_email_method": "link",
"reset_password_method": "code",
"disable_signup": false,
"password": {
"min_length": 8,
"require_number": false,
"require_lowercase": true,
"require_uppercase": false,
"require_special_char": false
},
"smtp": {
"enabled": false,
"host": "",
"port": 587,
"username": "",
"sender_email": "",
"sender_name": "",
"min_interval_seconds": 60
}
},
"storage": { "max_file_size_mb": 100 },
"realtime": { "retention_days": null },
"schedules": { "retention_days": 7 },
"deployments": { "subdomain": "my-app" }
},
"skipped": []
}config plan:
{
"changes": [
{
"section": "auth",
"op": "modify",
"key": "allowed_redirect_urls",
"from": ["https://app.com"],
"to": ["https://app.com", "https://staging.app.com"]
}
],
"summary": { "add": 0, "modify": 1, "remove": 0, "kept": 0 },
"skipped": []
}config apply:
{
"plan": {
/* same shape as plan output */
},
"applied": [
/* DiffChange objects that were applied */
],
"skipped": [
{
"key": "storage.max_file_size_mb",
"reason": "your backend doesn't expose storage.max_file_size_mb — upgrade the project to apply this section"
}
]
}Common mistakes
| Mistake | What to do instead |
|---|---|
| Calling raw admin APIs directly for TOML-supported settings | Use config apply — it's version-aware; direct writes can silently drop on older backends |
Treating skipped[] as an error to retry | It's intentional; surface verbatim with the upgrade ask and stop |
Running config apply in --json mode without --yes | Add -y/--yes (global) or --auto-approve (subcommand alias — same effect); otherwise fails fast with CONFIRMATION_REQUIRED |
Re-running with --force to "fix" a skip | --force is only for export's overwrite gate; skips need a backend upgrade |
| Managing OAuth apps, email templates, storage buckets, realtime channels, secrets, functions, or deployment env vars via TOML | Use their dedicated dashboard or CLI flows; TOML only carries supported project config knobs |
Related
npx @insforge/cli metadata— read-only view of all backend config slices- insforge app-integration skill
auth/sdk-integration.md— how SDK code reads auth config at runtime
npx @insforge/cli create
Create a new InsForge project.
Syntax
npx @insforge/cli create [options]Options
| Option | Description |
|---|---|
--name <name> | Project name |
--org-id <id> | Organization ID |
--region <region> | Region: us-east, us-west, eu-central, ap-southeast |
--template <template> | Template: react, nextjs, empty |
--json | Non-interactive mode. Skips all value-collection prompts (including the "Directory name:" prompt) and errors out if any required flag is missing. Required for agent / CI use. |
Interactive Mode
Without flags, the command prompts for organization, project name, region, and template.
Non-Interactive Mode
For CI/CD or agent use, pass --json along with all required flags:
npx @insforge/cli create --json --name my-app --org-id org_123 --region us-east --template react--json skips value-collection prompts (text inputs like Directory name:, pickers like organization / region) and errors out if any required flag is missing. The -y flag is a different feature — it only auto-accepts Y/N confirmations and does NOT suppress value-collection prompts. For create specifically, --json alone is sufficient (there are no Y/N confirmations); for destructive commands like delete, agents should pass both --json and -y. Agents sandboxed from stdin (e.g., Codex) hang on any unsuppressed prompt — always pass --json for programmatic create.
What It Does
1. Creates the project via the InsForge Platform API 2. Waits for the project to become active (polls every 3s, timeout 120s) 3. Fetches the project's API key 4. Downloads template files (if not empty) 5. Installs InsForge Agent Skills via npx skills add insforge/agent-skills 6. Creates .insforge/project.json in the current directory
Output
Project details: ID, name, appkey, region, and OSS host URL.
Examples
# Interactive — prompts for everything
npx @insforge/cli create
# Non-interactive with all options (agents, CI)
npx @insforge/cli create --json --name blog-app --org-id org_abc --region us-east --template react
# Create with empty template (no frontend scaffolding)
npx @insforge/cli create --json --name api-only --org-id org_abc --region eu-central --template emptyNotes
- Requires authentication (
npx @insforge/cli loginfirst). - Creates
.insforge/project.jsonwhich links the directory to the project. - Agent skills are auto-installed into
.agents/skills/insforge/.
Database Access Control for InsForge
Overview
Row Level Security (RLS) provides defense-in-depth for data isolation. When implemented correctly, it prevents data leaks even if application code misses a filter. When implemented incorrectly, it creates false security confidence while data bleeds between users or tenants.
Core principle: RLS is your last line of defense, not your only one. Get it wrong and you have a data breach.
---
InsForge RLS Basics
InsForge uses three built-in PostgreSQL roles:
| Role | Description | When active |
|---|---|---|
anon | Unauthenticated users | No valid session token |
authenticated | Logged-in users | Valid session token present |
project_admin | Project admin | CLI db query, migrations, API-key/admin tasks |
The current user's ID is available via auth.uid(). All user foreign keys should reference auth.users(id).
Raw SQL from db query and migration files runs as project_admin. This role can manage and own objects in public; access to InsForge-managed schemas is restricted.
Schema Scope and Managed Modules
For generic application database work, create and modify app-owned objects in the public schema.
- Create, alter, drop, grant, revoke, index, trigger, function, view, and policy changes on
publicapplication objects. - Do not create custom schemas or write to InsForge-managed/system schemas such as
auth,storage,realtime,payments,graphql,extensions,pg_catalog,information_schema, orsystem, unless you are working on that specific feature module and its docs explicitly allow the operation. - It is allowed to reference built-in objects such as
auth.users(id)andauth.uid()from public tables or public RLS policies; do not modify those built-in objects. - Put RLS helper functions in
public, schema-qualify references such aspublic.team_membersandauth.uid(), and pinSECURITY DEFINERhelpers toSET search_path = pg_catalog, public, pg_temp. - InsForge migrations already run against
public; schema-qualified references keep helper functions explicit.
Managed table RLS belongs to the corresponding storage, realtime, or payments feature context. Use those feature docs when the task is specifically about those modules.
Minimal RLS Setup
-- 1. Create table
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 2. Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 3. Create policies
CREATE POLICY "anyone can read" ON posts
FOR SELECT TO anon, authenticated
USING (true);
CREATE POLICY "owners can insert" ON posts
FOR INSERT TO authenticated
WITH CHECK (user_id = (SELECT auth.uid()));
CREATE POLICY "owners can update" ON posts
FOR UPDATE TO authenticated
USING (user_id = (SELECT auth.uid()))
WITH CHECK (user_id = (SELECT auth.uid()));
CREATE POLICY "owners can delete" ON posts
FOR DELETE TO authenticated
USING (user_id = (SELECT auth.uid()));
-- 4. Grant SQL privileges to the roles that should pass through the policies
GRANT USAGE ON SCHEMA public TO anon, authenticated;
GRANT SELECT ON posts TO anon, authenticated;
GRANT INSERT, UPDATE, DELETE ON posts TO authenticated;
-- 5. Auto-update updated_at
CREATE TRIGGER posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION system.update_updated_at();Policies decide which rows a role may access after PostgreSQL has allowed the SQL operation. They do not grant SELECT, INSERT, UPDATE, or DELETE privileges.
InsForge grants broad DML privileges on public tables to anon and authenticated by default so RLS policies can decide row-level access. When the goal is narrower than the default operation or column surface, explicitly revoke the broad privilege before granting the exact access you want:
REVOKE UPDATE ON public.posts FROM anon, authenticated;
GRANT UPDATE (title) ON public.posts TO authenticated;If you revoke a privilege, a matching policy is no longer enough by itself; the role still needs the operation or column grant to reach the policy.
Design the Operation Surface First
Before writing policies, decide what each runtime role may do at the SQL operation level. RLS answers "which rows"; privileges and guards answer "which operations and columns".
For each table, list:
| Operation | Typical access-control question |
|---|---|
SELECT | Who may see full rows, and who only sees a projection? |
INSERT | Which user identity or tenant must new rows belong to? |
UPDATE | Which rows may be edited, and which fields must remain immutable? |
DELETE | Is deletion allowed, or should lifecycle state/soft delete be modeled? |
If an operation or field is narrower than InsForge's broad public-table runtime privileges, revoke the broad privilege first, then grant back the exact surface.
Guard Protected Fields Outside RLS Predicates
RLS policies filter candidate rows and validate the final row with WITH CHECK. They do not compare old and new column values. PostgreSQL policy expressions do not have OLD or NEW.
For protected fields such as owner_id, tenant_id, role columns, immutable foreign keys, billing fields, or status fields, use column privileges and/or a BEFORE UPDATE trigger guard. This keeps invariants true even if a future policy or grant becomes broader.
REVOKE UPDATE ON public.documents FROM anon, authenticated;
GRANT UPDATE (title, body) ON public.documents TO authenticated;
CREATE OR REPLACE FUNCTION public.prevent_document_owner_change()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.owner_id IS DISTINCT FROM OLD.owner_id THEN
RAISE EXCEPTION 'owner_id cannot be changed';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER prevent_document_owner_change
BEFORE UPDATE ON public.documents
FOR EACH ROW EXECUTE FUNCTION public.prevent_document_owner_change();For field-level update masks such as "members may edit content, managers may edit status, finance may edit billing code", use a BEFORE UPDATE trigger to compare OLD and NEW; use RLS to decide whether the caller can reach the row.
Model ACLs as Positive Capabilities
For owner/editor/viewer/member sharing systems, avoid one broad FOR ALL policy. Write separate policies for each operation and express the positive capability needed for that operation.
SELECT: owner or active read/edit share.UPDATE: owner or active edit share, with protected owner/tenant fields
guarded separately.
DELETE: usually owner/admin only.- Share mutation: usually owner/admin only; viewers and editors should not
reshare, revoke, or escalate themselves unless that is explicitly intended.
Cross-table ACL lookups commonly touch RLS-enabled tables. Put those lookups in SECURITY DEFINER helpers, pin their search_path, and schema-qualify referenced objects.
Separate Private Base Tables from Public Projections
When a table contains private JSON, billing fields, internal notes, or other sensitive columns, keep full-row base-table access narrow. Expose public data through a view or function that projects only safe fields.
Do not make the base table readable by everyone just to make a public view work. That exposes the full row through direct table reads. If callers need a public projection, design the projection explicitly and grant access to the projection, not the private base table.
WITH (security_invoker = true) makes a PostgreSQL 15+ view respect the caller's RLS on the base tables. Use it when the base-table RLS already allows the rows the view should expose. If the public projection intentionally exposes a subset of fields from rows whose full base rows are private, use a carefully projected view/function and keep direct base-table privileges and policies narrow.
---
Critical Vulnerabilities
1. Infinite Recursive RLS (CRITICAL — Causes OOM Crash)
This is the most dangerous RLS bug. When RLS policies on table A call a function that queries table B, and table B's RLS calls a function that queries table A (or itself), PostgreSQL enters infinite recursion until the server runs out of memory and is killed by the OS.
Real-world example:
companies → is_company_member() → queries company_memberships
→ RLS on company_memberships
→ is_company_consultant_or_admin()
→ company_role()
→ queries company_memberships (LOOP!)
→ OOM → SIGKILLHow to detect:
- Database connection hangs, then the server crashes
- PostgreSQL logs show
SIGKILLor out-of-memory errors EXPLAINon the query runs forever
The fix — use SECURITY DEFINER:
-- DANGEROUS: This function runs as the calling role, so RLS is enforced
-- on every table it touches — creating recursion risk
CREATE OR REPLACE FUNCTION is_company_member(company_uuid UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM company_memberships
WHERE company_id = company_uuid AND user_id = auth.uid()
);
$$ LANGUAGE sql STABLE;
-- SAFE: SECURITY DEFINER runs as the function owner (postgres),
-- bypassing RLS on queried tables and breaking the recursion
CREATE OR REPLACE FUNCTION is_company_member(company_uuid UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM public.company_memberships
WHERE company_id = company_uuid AND user_id = auth.uid()
);
$$ LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp;Rule: Any helper function called from an RLS policy should be `SECURITY DEFINER` when it queries RLS-enabled tables. This includes same-table lookups, parent/ancestor lookups, membership tables, ACL/share tables, and helper chains that would otherwise re-enter RLS. Keep helpers in public, use explicit schema-qualified references, and pin the function search_path to pg_catalog, public, pg_temp.
Checklist:
- [ ] Map all RLS policy → function → table dependencies
- [ ] Every policy helper that queries RLS-enabled tables, including same-table lookups, is
SECURITY DEFINER - [ ] Every
SECURITY DEFINERhelper setssearch_pathtopg_catalog, public, pg_temp - [ ] Helper functions and policies schema-qualify app tables/functions with
public.and built-ins with their managed schema, such asauth.uid() - [ ] No circular chains: table A RLS → table B RLS → table A RLS
- [ ] If recursion or bad plans are suspected, use targeted
EXPLAIN
2. Missing USING or WITH CHECK (HIGH)
USING filters reads; WITH CHECK validates writes. Missing WITH CHECK allows inserting rows you can't read back.
-- INCOMPLETE: User can INSERT rows for other users
CREATE POLICY "owner access" ON posts
FOR ALL USING (user_id = auth.uid());
-- COMPLETE: Both read and write protected
CREATE POLICY "owner access" ON posts
FOR ALL
USING (user_id = (SELECT auth.uid()))
WITH CHECK (user_id = (SELECT auth.uid()));Checklist:
- [ ] INSERT/UPDATE policies always include
WITH CHECK - [ ]
FOR ALLpolicies include bothUSINGandWITH CHECK
3. Overly Permissive Policies (HIGH)
Multiple policies on the same table are combined with OR. One overly broad policy defeats all others.
-- DANGEROUS: This single policy overrides all restrictions
CREATE POLICY "allow all reads" ON orders
FOR SELECT USING (true);
CREATE POLICY "tenant isolation" ON orders
FOR SELECT USING (tenant_id = (SELECT auth.uid()));
-- ^ This is useless — the first policy already allows everythingChecklist:
- [ ] Audit all policies per table — they combine with OR
- [ ] No
USING (true)on sensitive tables unless intentional (e.g., public blog posts)
4. View Bypass (MEDIUM)
Views run with the creator's privileges by default.
-- DANGEROUS: View owned by superuser bypasses RLS
CREATE VIEW all_orders AS SELECT * FROM orders;
-- SAFE (PostgreSQL 15+): Respects caller's RLS
CREATE VIEW user_orders
WITH (security_invoker = true)
AS SELECT * FROM orders;---
Performance Considerations
Index Policy Columns
Every column referenced in an RLS policy should be indexed:
CREATE INDEX idx_posts_user_id ON posts(user_id);Wrap Functions in Subqueries
Functions called per-row are expensive. Wrap in a subquery for single evaluation:
-- SLOW: auth.uid() called per row
CREATE POLICY "owner access" ON posts
USING (user_id = auth.uid());
-- FASTER: Evaluated once
CREATE POLICY "owner access" ON posts
USING (user_id = (SELECT auth.uid()));Use SECURITY DEFINER for Cross-Table Checks
Avoid RLS-on-RLS chains (see Infinite Recursive RLS above). Wrap cross-table lookups in SECURITY DEFINER functions:
CREATE OR REPLACE FUNCTION user_accessible_document_ids(uid UUID)
RETURNS SETOF UUID AS $$
SELECT document_id FROM public.permissions WHERE user_id = uid;
$$ LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp;
CREATE POLICY "access check" ON documents
USING (id IN (SELECT * FROM user_accessible_document_ids((SELECT auth.uid()))));Denormalize for Performance
Store user_id or tenant_id directly on every table instead of relying on joins:
-- SLOW: Must join to resolve ownership
CREATE POLICY "item access" ON order_items
USING (order_id IN (
SELECT id FROM orders WHERE user_id = auth.uid()
));
-- FAST: Direct column check
ALTER TABLE order_items ADD COLUMN user_id UUID REFERENCES auth.users(id);
CREATE POLICY "item access" ON order_items
USING (user_id = (SELECT auth.uid()));---
Common InsForge RLS Patterns
Public Read, Owner Write
CREATE POLICY "public read" ON posts
FOR SELECT TO anon, authenticated
USING (true);
CREATE POLICY "owner write" ON posts
FOR INSERT TO authenticated
WITH CHECK (user_id = (SELECT auth.uid()));
CREATE POLICY "owner update" ON posts
FOR UPDATE TO authenticated
USING (user_id = (SELECT auth.uid()))
WITH CHECK (user_id = (SELECT auth.uid()));
CREATE POLICY "owner delete" ON posts
FOR DELETE TO authenticated
USING (user_id = (SELECT auth.uid()));
GRANT USAGE ON SCHEMA public TO anon, authenticated;
GRANT SELECT ON posts TO anon, authenticated;
GRANT INSERT, UPDATE, DELETE ON posts TO authenticated;Role-Based Access with Helper Function
CREATE OR REPLACE FUNCTION is_org_member(org_uuid UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members
WHERE org_id = org_uuid AND user_id = auth.uid()
);
$$ LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp; -- prevents recursive RLS and pins name resolution
CREATE POLICY "org members access" ON projects
FOR ALL TO authenticated
USING (is_org_member(org_id))
WITH CHECK (is_org_member(org_id));
GRANT USAGE ON SCHEMA public TO authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO authenticated;Authenticated-Only Access
CREATE POLICY "authenticated users only" ON profiles
FOR SELECT TO authenticated
USING ((SELECT auth.uid()) IS NOT NULL);
GRANT USAGE ON SCHEMA public TO authenticated;
GRANT SELECT ON profiles TO authenticated;---
Checklist
Before completing an RLS implementation:
- [ ] All tables with user data have
ALTER TABLE ... ENABLE ROW LEVEL SECURITY - [ ] Matching SQL privileges are granted to
anon/authenticated(GRANT USAGE ON SCHEMA ...,GRANT SELECT/INSERT/UPDATE/DELETE ON ...) - [ ] All policies have both
USINGandWITH CHECKwhere applicable - [ ] Protected owner, tenant, role, and identity fields are guarded with column privileges or triggers, not only RLS predicates
- [ ] No circular RLS dependencies between tables (infinite recursion risk)
- [ ] All policy helpers that query RLS-enabled tables are
SECURITY DEFINER - [ ] Helper functions pin
search_pathtopg_catalog, public, pg_temp - [ ] Helper functions and policies use explicit
public.and managed-schema references instead of relying onsearch_path - [ ] Broad default table privileges are revoked before narrower operation or column grants
- [ ] Policy columns (
user_id,tenant_id, etc.) are indexed - [ ]
(SELECT auth.uid())used in subquery form for performance - [ ] Public projections do not expose private base-table rows; view/function grants are separated from full table access
- [ ] No overly permissive
USING (true)on sensitive tables - [ ] Runtime behavior is not inferred from
project_admin-only queries
References
npx @insforge/cli db export
Export database schema and/or data.
Syntax
npx @insforge/cli db export [options]Options
| Option | Description | Default |
|---|---|---|
--format <format> | sql or json | sql |
--tables <tables> | Comma-separated table list | all tables |
--no-data | Export schema only (no row data) | include data |
--include-functions | Include stored functions | no |
--include-sequences | Include sequences | no |
--include-views | Include views | no |
--row-limit <n> | Max rows per table | unlimited |
-o, --output <file> | Output file path | stdout |
Examples
# Full export to file
npx @insforge/cli db export --output backup.sql
# Schema only
npx @insforge/cli db export --no-data --output schema.sql
# Specific tables with functions
npx @insforge/cli db export --tables users,posts --include-functions --output partial.sql
# JSON format
npx @insforge/cli db export --format json --output backup.json
# Limited rows for development
npx @insforge/cli db export --row-limit 100 --output dev-data.sqlnpx @insforge/cli db import
Import database from a SQL file.
Syntax
npx @insforge/cli db import <file> [options]Options
| Option | Description |
|---|---|
--truncate | Truncate existing tables before import |
Examples
# Import SQL file
npx @insforge/cli db import backup.sql
# Import with table truncation
npx @insforge/cli db import backup.sql --truncateOutput
Displays filename, number of tables processed, and rows imported.
Notes
- The file must be a valid SQL file (e.g., from
npx @insforge/cli db export). - Use
--truncatecarefully — it removes all existing data from tables before importing.
Database Integrity
Use this reference when a migration must enforce database invariants: counters, balances, latest pointers, append-only history, lifecycle states, quotas, protected deletes, immutable ownership fields, or trigger-maintained columns.
DDL belongs in a migration. Use SQL constraints for row-local invariants, unique indexes for uniqueness, foreign keys for references, and triggers only when the rule depends on transitions, related rows, or server-maintained derived state.
Choose the Smallest Database Primitive
| Need | Prefer |
|---|---|
| Required field or valid range | NOT NULL / CHECK |
| Unique active value | partial unique index |
| Parent-child reference | foreign key plus index on the referencing column |
| Immutable owner or tenant | BEFORE UPDATE guard trigger |
| Append-only history | revoke client UPDATE/DELETE, plus optional guard trigger |
| Counter, balance, latest pointer, current status | trusted trigger-maintained derived field |
| Cross-row state transition | trigger or SQL function with clear transition checks |
Server-Maintained Derived Fields
Derived fields include comment_count, balance_cents, latest_revision_id, current_status, last_event_at, and similar values maintained by database logic. Design them so normal client writes still work, but direct client edits to the derived value do not.
Required shape:
1. A legal client can create the parent row with defaults, NULL, or zero for server-maintained fields. 2. A legal client can create the child/event row that should update the parent. 3. The trigger updates the derived parent field. 4. A client cannot directly update the derived field. 5. Guard triggers must not block the trusted maintenance path.
Bad: Guard Blocks Its Own Maintenance Trigger
CREATE OR REPLACE FUNCTION public.protect_post_fields()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.comment_count IS DISTINCT FROM OLD.comment_count THEN
RAISE EXCEPTION 'comment_count is server maintained';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER protect_post_fields
BEFORE UPDATE ON public.posts
FOR EACH ROW EXECUTE FUNCTION public.protect_post_fields();
CREATE OR REPLACE FUNCTION public.bump_comment_count()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE public.posts
SET comment_count = comment_count + 1
WHERE id = NEW.post_id;
RETURN NEW;
END;
$$;The child insert fires bump_comment_count, which updates posts, which fires protect_post_fields, which rejects the legitimate maintenance update.
Good: Restrict Client Update Surface, Let Trigger Maintain
CREATE TABLE public.posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
comment_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE public.comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "owners can create posts"
ON public.posts FOR INSERT TO authenticated
WITH CHECK (owner_id = (SELECT auth.uid()));
CREATE POLICY "owners can edit post title"
ON public.posts FOR UPDATE TO authenticated
USING (owner_id = (SELECT auth.uid()))
WITH CHECK (owner_id = (SELECT auth.uid()));
GRANT SELECT, INSERT ON public.posts TO authenticated;
REVOKE UPDATE ON public.posts FROM authenticated;
GRANT UPDATE (title) ON public.posts TO authenticated;
CREATE OR REPLACE FUNCTION public.bump_comment_count()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp
AS $$
BEGIN
UPDATE public.posts
SET comment_count = comment_count + 1
WHERE id = NEW.post_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER comments_bump_post_count
AFTER INSERT ON public.comments
FOR EACH ROW EXECUTE FUNCTION public.bump_comment_count();Here the client has no column privilege to update comment_count directly, but the trusted trigger function can maintain it.
Legal Insert Payloads
InsForge gives runtime roles broad default DML privileges on public tables so RLS can decide row access. For integrity rules that narrow writes, explicitly REVOKE broad privileges before adding column-level or operation-specific GRANTs.
Column-level grants can accidentally block legitimate API payloads. Before using column-level INSERT grants, list every column a normal SDK/REST caller may send.
Avoid this when callers may send balance_cents: 0:
GRANT INSERT (id, owner_id, name) ON public.accounts TO authenticated;Prefer allowing the legal create payload and protecting later mutation:
GRANT SELECT, INSERT ON public.accounts TO authenticated;
REVOKE UPDATE ON public.accounts FROM authenticated;
GRANT UPDATE (name) ON public.accounts TO authenticated;The same rule applies to latest_revision_id = NULL, comment_count = 0, current_status = 'draft', and other server-maintained initial values.
Immutable Fields and Append-Only Tables
Guard fields that must never change after creation: owner_id, tenant_id, business identifiers, immutable slugs, or ledger account IDs.
CREATE OR REPLACE FUNCTION public.prevent_owner_change()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.owner_id IS DISTINCT FROM OLD.owner_id THEN
RAISE EXCEPTION 'owner_id cannot be changed';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER prevent_owner_change
BEFORE UPDATE ON public.documents
FOR EACH ROW EXECUTE FUNCTION public.prevent_owner_change();For append-only rows such as revisions, ledger entries, audit events, and claims:
REVOKE UPDATE, DELETE ON public.ledger_entries FROM authenticated;
GRANT SELECT, INSERT ON public.ledger_entries TO authenticated;Add trigger guards only if privileged or future grants might otherwise mutate history.
Latest Pointer and History Pattern
For document revisions or status history, keep history append-only and maintain a latest pointer on the parent.
CREATE OR REPLACE FUNCTION public.set_latest_revision()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public, pg_temp
AS $$
BEGIN
UPDATE public.documents
SET latest_revision_id = NEW.id
WHERE id = NEW.document_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER revisions_set_latest
AFTER INSERT ON public.document_revisions
FOR EACH ROW EXECUTE FUNCTION public.set_latest_revision();Do not add a parent guard that rejects every latest_revision_id change unless it also allows this trusted transition. The simpler pattern is to prevent client updates to that column with privileges and let the trigger maintain it.
Self-Check Before Finishing
- Can a legal parent insert pass with default,
NULL, or zero derived values? - Can a legal child/event insert pass?
- Does the child/event insert update the parent derived field?
- Is direct client mutation of derived fields blocked?
- Are immutable owner, tenant, and business identity fields protected?
- Are append-only child/history rows protected from update and delete?
- Do trigger functions that must bypass runtime privileges use
SECURITY DEFINER
with SET search_path = pg_catalog, public, pg_temp and schema-qualify references such as public.documents?
- Do RLS helpers that query RLS-enabled tables use
SECURITY DEFINERand
SET search_path = pg_catalog, public, pg_temp, then schema-qualify references such as public.team_members and auth.uid()?
- Are foreign keys and columns used by guards, RLS, and lookups indexed where
the table can grow?
npx @insforge/cli db migrations
Manage developer database migration files for an InsForge project.
Commands
npx @insforge/cli db migrations list
npx @insforge/cli db migrations fetch
npx @insforge/cli db migrations new <migration-name>
npx @insforge/cli db migrations up <migration-file-name-or-version>
npx @insforge/cli db migrations up --to <migration-file-name-or-version>
npx @insforge/cli db migrations up --allWhat Each Command Does
| Command | Description |
|---|---|
list | Show applied remote migrations (version, name, created date) |
fetch | Download remote applied migrations into migrations/ |
new <migration-name> | Create the next local migration file with the next timestamp version |
| `up <filename\\ | version>` |
| `up --to <filename\\ | version>` |
up --all | Apply every pending local migration file |
Filename Format
Migration files must be named exactly:
<migration_version>_<migration-name>.sqlExamples:
- valid:
20260418091500_create-users.sql - valid:
20260418103045_add-post-index.sql - invalid:
20260418_create-users.sql - invalid:
20260418091500_create_users.sql - invalid:
20260418091500_CreateUsers.sql - invalid:
20260418091500 create-users.sql
Migration Name Rules
The <migration-name> portion must use:
- lowercase letters
- numbers
- hyphens
No spaces, underscores, uppercase letters, or other special characters.
Local Directory
Migration files live under:
migrations/Examples
# View remote migration history
npx @insforge/cli db migrations list
# Fetch remote migration files into migrations/
npx @insforge/cli db migrations fetch
# Create the next migration file
npx @insforge/cli db migrations new create-posts
# Apply by exact filename
npx @insforge/cli db migrations up 20260418091500_create-posts.sql
# Apply by version
npx @insforge/cli db migrations up 20260418091500
# Apply all pending migrations through a target
npx @insforge/cli db migrations up --to 20260418110000
# Apply all pending migrations
npx @insforge/cli db migrations up --all
# JSON output
npx @insforge/cli db migrations list --jsonOutput
listprints a table with version, name, and created datefetchreports how many files were created and skippednewprints the created filenameupprints the applied filename(s) on success
Command Behavior
list
- Reads the current remote migration history from the project backend
- Shows only applied remote migrations
fetch
- Ensures
migrations/exists - Writes one local
.sqlfile per applied remote migration - Skips existing file paths without overwriting them, even if the contents differ
new <migration-name>
- Validates the migration name
- Looks at the latest remote migration version
- Validates local filenames before choosing the next timestamp version
- Uses the greater of current UTC time or the latest known local/remote version, bumping by one second when needed
- Fails if local migration filenames are malformed or duplicated
up <filename|version>
- Resolves exactly one local file target
- Applies exactly one migration file
- The target must be the next pending local migration after the latest remote version
- Fails if the target is ambiguous, missing, empty, invalidly named, or already applied
- Unrelated invalid files elsewhere in
migrations/do not block an explicit valid target
up --to <filename|version>
- Strictly validates every local migration filename first
- Applies pending local migrations in ascending version order
- Stops after the chosen target migration is applied
- Fails if the target is missing, already applied, ambiguous, or not present in the pending set
up --all
- Strictly validates every local migration filename first
- Applies every pending local migration in ascending version order
- Stops on the first failure
Best Practices
1. Use migrations for schema changes
- Migration SQL runs as
project_admin. project_admincan manage and own objects inpublic, but access to InsForge-managed schemas is restricted.- For generic application database work, create and evolve app-owned objects through migration files in
public: tables, views, indexes, policies, triggers, helper functions, and grants. - Do not create custom schemas or write to InsForge-managed/system schemas such as
auth,storage,realtime,payments,graphql,extensions,pg_catalog,information_schema, orsystem, unless you are working on that specific feature module and its docs explicitly allow the operation. - It is allowed to reference built-in objects such as
auth.users(id)andauth.uid()from public tables or public RLS policies; do not modify those built-in objects. - Group related schema changes into one migration when practical.
- Reserve
db queryfor row-level data fixes, backfills, and targeted inspection. - Migration apply reloads the PostgREST schema cache automatically.
- Migration SQL runs against
public; schema-qualify references such aspublic.postsandauth.uid().
2. Normalize large JSONB payloads into columns or child tables
- Avoid designing tables where app code reads/writes large JSONB blobs through PostgREST; large JSONB rows can drive excessive PostgREST memory use.
- Use typed columns for fields used in filters, sorting, list views, RLS policies, or partial updates.
- Use child tables for repeated nested objects, with foreign keys and indexes on ownership/lookup columns.
- Keep JSONB for small, rarely queried metadata/config where whole-object reads and writes are acceptable.
3. Compare remote and local migration history
- Use
listto see applied remote migrations. - Use
fetchto sync applied remote migration files intomigrations/.
4. Use `new` instead of naming files by hand
- Let the CLI assign the next timestamp version safely.
5. Use explicit single-target apply for focused changes
up <filename>orup <version>is ideal when you want one specific migration.
6. Use batch apply for CI or bootstrap
up --to <target>orup --allis safer than hand-looping files in shell scripts because the CLI keeps ordering and fail-fast behavior consistent.
7. Treat fetched files as history
- Once a migration is applied remotely, avoid editing its local file.
8. Do not include transaction statements in migration files
- The backend executes each migration inside its own transaction.
- Do not add
BEGIN,COMMIT, orROLLBACKto the migration SQL.
Common Mistakes
| Mistake | Solution |
|---|---|
| Naming files manually with underscores or spaces | Use npx @insforge/cli db migrations new <migration-name> |
Reaching for db query to create or alter schema | Use migration files for schema changes; reserve db query for row changes |
| Trying to alter InsForge-managed tables like app-owned tables | Keep generic schema, RLS, trigger, function, and grant changes on public application objects; use feature-specific docs for managed module hooks or RLS |
| Storing large app state or repeated nested objects in one JSONB column | Normalize into typed columns and child tables before exposing the table through SDK/PostgREST CRUD |
| Applying a file out of order | Apply the next pending local migration, or fix/delete the earlier local file that is blocking it |
| Keeping a local file older than the current remote head | Rename it with a newer timestamp or delete it locally if it is stale |
Adding BEGIN / COMMIT / ROLLBACK to migration SQL | Remove them; the backend already wraps the migration in its own transaction |
| Editing already-fetched remote history casually | Treat fetched files as applied history, not drafts |
Assuming fetch overwrites local files | fetch skips existing file paths instead of replacing them |
Recommended Workflow
1. Check remote history → npx @insforge/cli db migrations list
2. Sync applied files when useful → npx @insforge/cli db migrations fetch
3. Create the next migration file → npx @insforge/cli db migrations new <migration-name>
4. Edit the SQL file → migrations/<version>_<migration-name>.sql
5. Apply the migration → npx @insforge/cli db migrations up <filename> or --all
6. If apply fails, read the error, fix the migration, and retry the migration.npx @insforge/cli db query
Execute a raw SQL query against the project database for targeted inspection and row-level data changes.
Syntax
npx @insforge/cli db query <sql> [options]Options
| Option | Description |
|---|---|
--json | Return rows as JSON for scripting |
Examples
# Basic query
npx @insforge/cli db query "SELECT * FROM posts LIMIT 10"
# Update rows
npx @insforge/cli db query "UPDATE posts SET status = 'published' WHERE id = 'post_123'"
# Insert rows
npx @insforge/cli db query "INSERT INTO posts (title, status) VALUES ('Hello', 'draft')"
# Delete rows
npx @insforge/cli db query "DELETE FROM posts WHERE archived = true"
# Inspect Postgres system catalog
npx @insforge/cli db query "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = 'public'"
# Inspect InsForge-managed schema data
npx @insforge/cli db query "SELECT * FROM auth.users LIMIT 10"
# JSON output for scripting
npx @insforge/cli db query "SELECT count(*) FROM posts" --jsonOutput
- Human: Formatted table
- JSON:
{ "rows": [...] }
Permission Model and Schema Changes
db query runs as project_admin.
public: full access for normal data changes and schema work.- Postgres system catalogs such as
pg_catalogandinformation_schema: read-only inspection is allowed. - InsForge-managed/system schemas such as
auth,storage,realtime,payments,graphql,extensions,pg_catalog,information_schema, orsystem: do not write or run DDL unless you are working on that specific feature module and its docs explicitly allow the operation.
Use npx @insforge/cli db migrations new ... and npx @insforge/cli db migrations up ... for schema changes on public application objects.
Use db query for:
- reading app data and inspecting managed-schema data
- inspecting Postgres system catalogs such as
pg_catalogandinformation_schema - backfilling or correcting rows in
public - one-off row updates in
public
For schema, RLS, grants, triggers, functions, indexes, and extensions, create a migration and apply it.
InsForge SQL References
When writing SQL for InsForge, use these built-in references:
| Reference | Description |
|---|---|
auth.uid() | Returns current authenticated user's UUID (use in RLS policies) |
auth.users(id) | Built-in users table — use for foreign keys, not a custom table |
system.update_updated_at() | Built-in trigger function that auto-updates updated_at columns |
Complete Example: Row-Level Data Fix
# Inspect the current rows
npx @insforge/cli db query "SELECT id, status FROM posts WHERE status IS NULL"
# Backfill missing row values
npx @insforge/cli db query "UPDATE posts SET status = 'draft' WHERE status IS NULL"Notes
- For schema changes and RLS policy changes, use the migrations workflow in migrations.md.
- For advanced access-control patterns (RLS recursion prevention, SECURITY DEFINER, performance), see access-control.md.
Database Vector Search
Use this reference when configuring pgvector with the InsForge CLI: vector extension setup, embedding columns, similarity search functions, HNSW/IVFFlat indexes, and vector-specific RLS considerations.
For app code that generates embeddings through OpenRouter and inserts vectors with @insforge/sdk, use the insforge app-integration skill's AI/RAG guidance after this backend schema is in place.
Migration Pattern
DDL belongs in a migration. Create a migration file with npx @insforge/cli db migrations new <name>, put SQL like the example below in that file, then apply it with npx @insforge/cli db migrations up --all.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE public.documents (
id BIGSERIAL PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL,
embedding_model TEXT NOT NULL DEFAULT 'openai/text-embedding-3-small',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "owners can read documents"
ON public.documents
FOR SELECT TO authenticated
USING (owner_id = (SELECT auth.uid()));
CREATE POLICY "owners can insert documents"
ON public.documents
FOR INSERT TO authenticated
WITH CHECK (owner_id = (SELECT auth.uid()));
GRANT SELECT, INSERT ON public.documents TO authenticated;
CREATE OR REPLACE FUNCTION public.match_documents(
query_embedding vector(1536),
match_count INT DEFAULT 5,
match_threshold DOUBLE PRECISION DEFAULT 0.78
)
RETURNS TABLE (
id BIGINT,
content TEXT,
similarity DOUBLE PRECISION
)
LANGUAGE sql
STABLE
SECURITY INVOKER
AS $$
SELECT
public.documents.id,
public.documents.content,
1 - (public.documents.embedding <=> query_embedding) AS similarity
FROM public.documents
WHERE 1 - (public.documents.embedding <=> query_embedding) >= match_threshold
ORDER BY public.documents.embedding <=> query_embedding
LIMIT match_count;
$$;
GRANT EXECUTE ON FUNCTION public.match_documents(vector, INT, DOUBLE PRECISION)
TO authenticated;
CREATE INDEX documents_owner_id_idx ON public.documents (owner_id);
CREATE INDEX documents_embedding_hnsw_idx
ON public.documents
USING hnsw (embedding vector_cosine_ops);Dimensions
Match vector(N) to the embedding model output dimension.
| Model | Dimensions |
|---|---|
openai/text-embedding-3-small | 1536 |
openai/text-embedding-3-large | 3072 |
openai/text-embedding-ada-002 | 1536 |
google/gemini-embedding-001 | 3072 |
A vector column's dimension cannot be altered in place. To change models with a different dimension, create a new vector column/table and re-embed data.
Distance Operators
Pick one distance operator and use the matching index operator class.
| Operator | Distance | Operator class | Typical use |
|---|---|---|---|
<=> | Cosine | vector_cosine_ops | Default for normalized embeddings |
<-> | L2 | vector_l2_ops | Un-normalized embeddings |
<#> | Inner product, negated | vector_ip_ops | Advanced ranking patterns |
For cosine distance, lower distance is closer. If exposing a similarity score, use 1 - (embedding <=> query_embedding) and keep ordering by raw distance.
Indexing
Without an index, pgvector performs exact nearest-neighbor scans. That is correct but linear. Add an index before production-sized workloads.
HNSW is usually the default choice and is safe to create on empty tables:
CREATE INDEX documents_embedding_hnsw_idx
ON public.documents
USING hnsw (embedding vector_cosine_ops);IVFFlat uses less memory, but build it only after representative data exists:
CREATE INDEX documents_embedding_ivfflat_idx
ON public.documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);Index columns used with vector filters, such as owner_id, tenant_id, document_type, or created_at. The vector index helps nearest-neighbor order; normal B-tree indexes help metadata filters.
SQL Inserts and Queries
For small SQL fixtures or debugging, cast a JSON-array literal to the exact vector dimension:
CREATE TABLE public.vec_demo (
id BIGSERIAL PRIMARY KEY,
embedding vector(3) NOT NULL
);
INSERT INTO public.vec_demo (embedding)
VALUES ('[0.12,0.34,0.56]'::vector(3));
SELECT *
FROM public.vec_demo
ORDER BY embedding <=> '[0.10,0.30,0.55]'::vector(3)
LIMIT 5;For real app data, generate embeddings in server-side app code and insert a number[] with the InsForge SDK.
RLS and RPCs
Standard RLS applies to vector tables. A SECURITY INVOKER match function runs under the caller's role, so table policies still filter rows.
If a vector search function must be SECURITY DEFINER, re-check auth.uid() or tenant membership inside the function body. Do not bypass RLS and return vectors or documents across users/tenants by accident.
Common Mistakes
| Mistake | Fix |
|---|---|
Creating pgvector extension instead of vector | Use CREATE EXTENSION IF NOT EXISTS vector; |
| Dimension mismatch between model and column | Set vector(N) to the model's exact output dimension |
| Ordering similarity descending while thresholding distance | Keep distance and similarity semantics explicit |
| Operator class does not match query operator | Pair <=> with vector_cosine_ops, <-> with vector_l2_ops, <#> with vector_ip_ops |
| IVFFlat on an empty table | Use HNSW, or build IVFFlat after representative rows exist |
| Client-side distance math | Put search/ranking in SQL or an RPC |
| SECURITY DEFINER vector RPC without user/tenant filter | Re-filter inside the function body |
npx @insforge/cli deployments deploy — frontend hosting (Vercel)
Deploy a frontend project (static site / SPA / Next.js / etc.) to InsForge hosting (via Vercel) from its source directory.
Looking to deploy a backend Docker container (API, worker)? Use
npx @insforge/cli compute deploy instead — seecompute-deploy.md.
Syntax
npx @insforge/cli deployments deploy [source-directory] [options]Options
| Option | Description |
|---|---|
--env <vars> | Environment variables as JSON: '{"KEY":"value"}' |
--meta <meta> | Metadata as JSON |
Default Directory
Current directory (.) if not specified. In most projects, this should be the app root, not dist/, build/, or .next/.
What It Does
1. Creates a deployment record (gets presigned upload URL) 2. Zips the source directory (max compression) 3. Uploads the zip to the presigned URL 4. Starts the deployment with env vars and metadata 5. Polls status every 5 seconds for up to 2 minutes 6. Returns the live URL and deployment ID
Excluded Files
The following are automatically excluded from the upload:
node_modules/,.git/,.next/,dist/,build/.env*,.DS_Store,.insforge/,*.log
Because build output is excluded automatically, deploy the project source tree/root directory instead of pointing the command at dist/, build/, or .next/.
Custom excludes with .vercelignore
To exclude additional files, add a .vercelignore file to the deploy source directory. It uses .gitignore syntax, including ! negation:
# .vercelignore
*.md
drafts/
!IMPORTANT.mdNotes:
- Patterns apply on top of the built-in excludes above; the built-ins always stay excluded and cannot be re-included with
!. - The
.vercelignorefile itself is never uploaded. - If the project was previously deployed directly with the Vercel CLI, an existing
.vercelignoreis honored as-is.
Environment Variables Are Required
Frontend apps need env vars (API URL, anon key, etc.) to connect to InsForge. Deploying without them produces a broken app. There are two ways to provide them:
Option A — Persistent env vars (recommended for repeated deploys):
# Check what's already configured
npx @insforge/cli deployments env list
# Set env vars — these persist across all future deployments
npx @insforge/cli deployments env set VITE_INSFORGE_URL https://my-app.us-east.insforge.app
npx @insforge/cli deployments env set VITE_INSFORGE_ANON_KEY ik_xxx
# Deploy the project source — persistent vars are applied automatically
npx @insforge/cli deployments deploy .Option B — Inline `--env` flag (one-off or override):
npx @insforge/cli deployments deploy . --env '{"VITE_INSFORGE_URL": "https://my-app.us-east.insforge.app", "VITE_INSFORGE_ANON_KEY": "ik_xxx"}'Before deploying, always confirm env vars are in place: either check deployments env list shows the required vars, or pass --env on the command.
Examples
# Deploy with persistent env vars already set
npx @insforge/cli deployments deploy .
# Deploy with inline env vars
npx @insforge/cli deployments deploy . --env '{"VITE_API_URL": "https://my-app.us-east.insforge.app", "VITE_ANON_KEY": "ik_xxx"}'
# JSON output
npx @insforge/cli deployments deploy . --jsonTypical Workflow
Pre-Deployment: Local Build Verification
CRITICAL: Always verify local build succeeds before deploying to InsForge.
Local builds are faster to debug and don't waste server resources on avoidable errors.
After the build passes locally, deploy the project source directory (usually .). Do not deploy the generated output folder; the CLI excludes it automatically.
Local Build Checklist:
# 1. Install dependencies
npm install
# 2. Create production environment file
# Use the correct prefix for your framework:
# - Vite: VITE_INSFORGE_URL
# - Next.js: NEXT_PUBLIC_INSFORGE_URL
# - CRA: REACT_APP_INSFORGE_URL
# - Astro: PUBLIC_INSFORGE_URL
cat > .env.production << 'EOF'
INSFORGE_URL=https://your-project.insforge.app
INSFORGE_ANON_KEY=your-anon-key
EOF
# 3. Run production build
npm run buildCommon Build Errors & Solutions
| Error | Cause | Solution |
|---|---|---|
| Missing env var errors | Build-time env vars not set | Create .env.production with framework-specific prefix |
| Module resolution errors | Edge functions scanned by compiler | Exclude edge function directories from build config |
| Static export conflicts | Dynamic routes with static export | Use SSR or configure static params per framework docs |
module_not_found | Missing dependency | Run npm install and verify package.json |
Framework-Specific Notes
Environment Variables by Framework:
| Framework | Prefix | Example |
|---|---|---|
| Vite | VITE_ | VITE_INSFORGE_URL |
| Next.js | NEXT_PUBLIC_ | NEXT_PUBLIC_INSFORGE_URL |
| Create React App | REACT_APP_ | REACT_APP_INSFORGE_URL |
| Astro | PUBLIC_ | PUBLIC_INSFORGE_URL |
| SvelteKit | PUBLIC_ | PUBLIC_INSFORGE_URL |
Edge Functions: If your project has edge functions in a separate directory (commonly functions/ for Deno-based functions), exclude them from your frontend build to prevent module resolution errors. Add the directory to your TypeScript or bundler exclude configuration.
Start to Deploy
# 4. Ensure env vars are set (check existing, add any missing)
npx @insforge/cli deployments env list
npx @insforge/cli deployments env set VITE_INSFORGE_URL https://my-app.us-east.insforge.app
npx @insforge/cli deployments env set VITE_INSFORGE_ANON_KEY ik_xxx
# 5. Deploy the project source (persistent env vars are applied automatically)
npx @insforge/cli deployments deploy .Alternatively, pass env vars inline: npx @insforge/cli deployments deploy . --env '{"VITE_INSFORGE_URL": "...", "VITE_INSFORGE_ANON_KEY": "..."}'
Check Deployment Status
Wait 30 seconds to 1 minute, then check status with npx @insforge/cli deployments status <id>.
Status Values
| Status | Description |
|---|---|
WAITING | Waiting for source upload |
UPLOADING | Uploading to build server |
QUEUED | Queued for build |
BUILDING | Building (typically ~1 min) |
READY | Complete - URL available |
ERROR | Build or deployment failed |
CANCELED | Deployment cancelled |
SPA Routing
For React single-page apps, ensure a vercel.json exists in the project root:
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}Best Practices
1. Deploy the project source directory
- Run the command from your app root, or pass that directory explicitly
- Do not deploy
dist/,build/, or.next/directly; the CLI excludes them automatically - Never include
node_modules,.git,.env, or.insforgein the upload - Large assets should go to InsForge Storage, not the deployment
2. Always set env vars before deploying
- Use
deployments env setfor persistent vars, or--envfor one-off deploys - Run
deployments env listto verify vars are in place before deploying - Never commit
.envfiles to source or include in zip - Use the correct env var prefix for your framework:
VITE_*,NEXT_PUBLIC_*,REACT_APP_*, etc.
3. Always build locally first to catch errors before deploying.
4. Include vercel.json for SPAs
- Required for client-side routing to work properly
Common Mistakes
| Mistake | Solution |
|---|---|
| Including node_modules in zip | Exclude it - will be installed during build |
| Including .env files | Use deployments env set or --env flag instead |
Deploying dist/, build/, or .next/ directly | Deploy the project source/root directory instead; the CLI excludes build output automatically |
| Deploying without env vars | Run deployments env list first — if empty, set vars with deployments env set or --env |
| Missing VITE_* env vars | Add all required build-time variables with correct framework prefix |
| Checking status too early | Wait 30sec-1min before checking status |
| Missing vercel.json for SPA | Add rewrites config for client-side routing |
npx @insforge/cli domains — custom domains
Use domains when a user wants to search, buy, attach, configure, verify, or resume custom domain setup through the InsForge CLI.
Cloudflare Connection
npx @insforge/cli domains cloudflare login- open Cloudflare OAuth and save the selected Cloudflare account plus OAuth token locally.- The Cloudflare OAuth client must include the
account-settings.readscope so the CLI can call/accountsand choose the Cloudflare account after authorization. --account-id <id>is for CI/admin automation only; do not make it the normal interactive product flow.--skip-browserprints the OAuth URL instead of trying to open a browser.- Do not ask users to create or paste Cloudflare API tokens for the normal flow.
Split Workflow
npx @insforge/cli domains search <query> [--limit <n>] [--tlds com,dev]- search Cloudflare Registrar.--tldsis only a local filter; do not assume a fixed TLD allowlist.npx @insforge/cli domains check <domain...>- check real-time availability and pricing.npx @insforge/cli domains buy <domain>- register in the connected Cloudflare account. Registration enables auto-renew and WHOIS redaction.npx @insforge/cli domains attach <domain>- attach to the linked InsForge deployment.npx @insforge/cli domains dns sync <domain>- write InsForge/Vercel DNS records to Cloudflare DNS.npx @insforge/cli domains verify <domain>- trigger InsForge custom-domain verification.npx @insforge/cli domains status <domain> [--cloudflare]- inspect InsForge status and optionally Cloudflare registration status.npx @insforge/cli domains resume <domain>- continue attach/DNS/verify after async registration finishes.npx @insforge/cli domains buy-and-attach <domain>- run register, attach, DNS sync, and verify in one flow.
Purchase Safety
- Before asking the user to confirm a purchase, remind them that the connected Cloudflare account must have a Registrar registrant contact/default address book entry and a valid payment method/billing profile. Without these, Cloudflare may fail during registration even after availability and pricing checks pass.
- Never rely on global
--yesfor domain purchases. Non-interactive registration requires all explicit flags:--confirm-domain,--confirm-price,--confirm-currency,--confirm-cloudflare-billing, and--confirm-non-refundable. - Successful domain registrations may be non-refundable. Confirm the exact domain and Cloudflare-returned price before buying.
- Cloudflare decides which TLDs are programmatically registrable. If a TLD is unsupported, report Cloudflare's availability/reason instead of inventing another provider flow.
- If Cloudflare returns
No registrant contact provided, tell the user to configure the account's Registrar contact/address book entry before retrying. - If Cloudflare returns
Failed to create a quote, first ask the user to check payment method, billing profile, tax/address details, and Registrar eligibility in Cloudflare before retrying. - After
domains buy-and-attachsucceeds, rundomains status <domain> --cloudflare --jsononce more before reporting completion; the immediate response can briefly showverified: truewithmisconfigured: truebefore DNS verification settles.
npx @insforge/cli login
Authenticate with the InsForge platform.
Syntax
npx @insforge/cli login [options]Options
| Option | Description |
|---|---|
--email | Use email/password login instead of OAuth |
--client-id <id> | Custom OAuth client ID |
Authentication Methods
OAuth (Default)
Opens your browser for OAuth 2.0 authentication with PKCE:
npx @insforge/cli loginThe CLI starts a local callback server, opens the browser, and waits up to 5 minutes for you to authorize.
Email/Password
npx @insforge/cli login --emailPrompts for email and password interactively. For non-interactive use (CI/CD), set environment variables:
INSFORGE_EMAIL=user@example.com INSFORGE_PASSWORD=secret npx @insforge/cli login --emailCredential Storage
Tokens are saved to ~/.insforge/credentials.json with restricted file permissions (0600). Includes:
access_tokenandrefresh_token- User info (id, name, email)
Tokens refresh automatically on 401 responses.
Examples
# Interactive OAuth login (recommended)
npx @insforge/cli login
# Email/password login
npx @insforge/cli login --email
# CI/CD non-interactive login
INSFORGE_EMAIL=$EMAIL INSFORGE_PASSWORD=$PASSWORD npx @insforge/cli login --email --jsonRelated skills
Forks & variants (1)
Insforge Cli has 1 known copy in the catalog totaling 312 installs. They canonicalize to this original listing.
- insforge - 312 installs
How it compares
Use insforge-cli for terminal infra and branch ops; use the insforge skill for OpenRouter audio and SDK application code.
FAQ
What can insforge-cli manage from the terminal?
insforge-cli manages InsForge projects, SQL, migrations, RLS, functions, storage, compute, secrets, OpenRouter and Stripe keys, schedules, logs, diagnostics, and import/export. Auth redirect URLs apply declaratively through insforge.toml with config apply.
When should developers use insforge-cli vs the insforge SDK skill?
Developers use insforge-cli for infrastructure, migrations, branches, and secrets from the terminal. The insforge skill targets application code using @insforge/sdk, such as OpenRouter audio integration in server handlers.
Is Insforge Cli safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.