# Naro > Naro deploys Next.js (and static / Vite / CRA, plus static builds of Astro, SvelteKit, Nuxt, Angular, Docusaurus and Gatsby) apps to Cloudflare Workers. > Push code, get a live URL — no dashboard required. Everything below works > from a terminal; an AI agent can run these commands directly. ## Deploy from the CLI (no install of anything else needed) The CLI is published on npm as `naro-cli` (bin: `naro`). Use it with npx: ```bash # 1. Sign in once (opens a browser for GitHub). Skips if already logged in. npx naro-cli login # 2. From the project directory, deploy. # First run auto-detects the framework, creates the project, links it # (.naro/project.json) and prints a live preview URL. npx naro-cli deploy # 3. Ship to production. npx naro-cli deploy --prod ``` Preview deploys resolve to `https://-preview.naro.sh`, production to `https://.naro.sh`. Preview URLs are protected by default: only members of the project can open one, after signing in to naro. A GET or HEAD that is a page navigation (`Sec-Fetch-Mode: navigate`), or that sends no `Sec-Fetch-Mode` and an `Accept` including `text/html`, is redirected (302) to the naro sign-in; every other request, such as an API call or a fetch that does not ask for HTML, gets `401 PREVIEW_PROTECTED`. So an agent that fetches a preview page with `Accept: text/html` receives the sign-in redirect, not a 401. A request the browser sends without cookies (a `` without `crossorigin="use-credentials"`) gets 401 as well; add that attribute. Send the project's bypass secret (`npx naro-cli protection bypass`, shown once) as the `x-naro-protection-bypass` header. Opening the preview to anyone with the link (`npx naro-cli protection disable --yes`) changes who can see the project: run it only after the user confirms. Without `--yes`, that command, a `bypass` that replaces a secret and `bypass remove` exit with `CONFIRMATION_REQUIRED` and change nothing. Production URLs and the Naro backend API (`/_naro/v1`) are never protected. ## For CI or headless agents (no browser) `npx naro-cli login` needs a browser. In CI, set a token instead: ```bash NARO_TOKEN=$(npx naro-cli token) npx naro-cli deploy --prod --yes --json ``` Every command accepts `--json` (one machine-readable JSON object on stdout, logs on stderr) and `--yes` (never prompt). Failures return a stable `{ "success": false, "code": "...", "message": "..." }` — codes include `NOT_LOGGED_IN`, `PROJECT_NOT_LINKED`, `FRAMEWORK_UNSUPPORTED`, `BUILD_FAILED`, `DEPLOYMENT_NOT_FOUND`. On `BUILD_FAILED`, read the build logs, fix the code, and redeploy. ## Common commands ```bash npx naro-cli deploy # preview deploy of the current directory npx naro-cli deploy --prod # production deploy npx naro-cli deployments # list deployments of the linked project npx naro-cli logs # runtime logs npx naro-cli logs --type build # build logs of the latest deployment npx naro-cli analytics # Web Analytics: visitors, page views and the top pages, referrers, countries, devices, browsers and operating systems (--range 30d) npx naro-cli analytics --country KR --os ios # narrow it: --path, --referrer, --country, --device, --browser, --os, one value each npx naro-cli insights # Speed Insights: Web Vitals p75 by page, device, element and deployment, and the page load (p75 and the mean of each step) (--metric inp, --range 30d) npx naro-cli insights --env preview # the same for the preview worker (one per project, every preview deploy overwrites it) npx naro-cli insights --country KR --device mobile # narrow it: --path, --country, --device, --browser, --os (Speed Insights has no --referrer) npx naro-cli protection # preview protection: on (members only) or off, and whether a bypass secret is set npx naro-cli protection disable --yes # open the preview URL to anyone with the link, only after the user confirms (enable protects it again) npx naro-cli protection bypass # a secret for automation to send as x-naro-protection-bypass (shown once; bypass remove deletes it) npx naro-cli env add KEY # add an env var (value via stdin or prompt) npx naro-cli env pull # write env vars to .env.local # Local .env files are never uploaded automatically. Deploy reports the env # vars your source references but the project lacks — set those with env add. npx naro-cli db enable # create the project's database (Cloudflare D1, bound as env.DB) npx naro-cli db query "select 1" # run SQL; --file q.sql, or pipe stdin npx naro-cli migration new create_users # migrations/0001_create_users.sql npx naro-cli db push --yes # apply pending migrations (each file all-or-nothing; asks without --yes) npx naro-cli db table create posts --column "id:integer:pk:autoincrement" --column "title:text:notnull" --column "created_at:datetime:notnull:default=current_timestamp" --owner user_id # a table without SQL, recorded as a migration and written to ./migrations npx naro-cli db table add-column posts --column "published:boolean:notnull:default=false" npx naro-cli db index create posts user_id,created_at # --unique, --name npx naro-cli db table drop posts --yes # also rename, rename-column, drop-column; db index drop; --dry-run prints the SQL npx naro-cli gen types --output src/database.types.ts # TypeScript types; its Database goes to createClient() npx naro-cli db dump --output backup.sql # SQL dump of the database, streamed from Cloudflare npx naro-cli db restore --timestamp 2026-09-21T14:35:22Z # Time Travel (last 30 days); asks without --yes npx naro-cli db pull # record an existing schema and its policies as migrations/0001_remote_schema.sql npx naro-cli db reset --yes # drop every table/view/trigger, clear the ledger and the policies, apply ./migrations again npx naro-cli db lint --strict --json # schema diagnostics; exits 1 on an error (--strict: on a warning too) npx naro-cli migration repair 0001_init.sql --status applied --yes # fix the ledger only; its SQL is not run npx naro-cli domains add example.com # adds www.example.com too (--no-www: the apex alone); up to 20 domains per project (409 DOMAIN_LIMIT_REACHED) npx naro-cli rollback # roll production back to the previous build npx naro-cli promote # promote a preview build to production ``` ## Project backend (early access) A project can have a backend: its own D1, sign-in (naro Auth, the project's own, like Supabase Auth: email or username and password, working with no setup), and a Data API that browser code calls through `naro-js` (`npm install naro-js`) at the project's URL. It is off until you turn it on, one project at a time — with `backend enable` or on the dashboard's Database page; creating a project sets nothing up. Until the backend is switched on for the control plane, these commands answer `BACKEND_DISABLED`. ```bash npx naro-cli backend enable # turn it on; safe to re-run, resumes a failed run (provision is the same command) npx naro-cli backend status # on or off, status, URL and the anon key (safe in browser code) npx naro-cli backend disable --yes # turn it off; data, users and keys are kept npx naro-cli backend keys --reveal # the service key: a secret that skips every policy npx naro-cli backend keys rotate --service --yes # replace a key; the old one stops working at once npx naro-cli backend auth signups off # nobody signs up on their own; only accounts you create npx naro-cli backend users create --username admin --password-stdin < password.txt # a confirmed account npx naro-cli backend users # id, username, email, verified, created npx naro-cli backend users delete admin --yes # by id, username or email; ends its sessions npx naro-cli backend auth password --min-length 10 # fewest characters of a new password (6-128) # Policies belong in the migration of their table (below). These change the # database directly: nothing is written to ./migrations, and naro db reset # removes every policy your migration files do not make. npx naro-cli policy list [--table todos] # every policy, as CREATE POLICY npx naro-cli policy create 'create policy "own todos" on todos to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid())' npx naro-cli policy create --file policies.sql # CREATE POLICY / DROP POLICY statements, in order npx naro-cli policy drop "own todos" --table todos --yes # Shorthand for the policies naro_select … naro_delete, from a rule or a condition: npx naro-cli policy set todos --select owner --insert owner --update owner --delete owner --owner user_id npx naro-cli policy set posts --select-when 'or(is_public.eq.true,user_id.eq.auth.uid())' --insert-when 'user_id.eq.auth.uid()' npx naro-cli policy remove todos --yes # drops every policy: the table is closed again ``` Write a table's policies in the migration that creates it, with the syntax Supabase uses. A table without a policy refuses every anon and signed-in request (only the service key gets through), so a new table is not finished until its migration has one. `naro db push`, `naro migration up`, `naro db query` and the MCP tools `apply_migration` and `execute_sql` take `CREATE POLICY` in the SQL they run and apply it together with the table, all or nothing. Three common shapes, each in the migration of its own table: ```sql -- migrations/0002_todos.sql: each signed-in user reads and changes their own rows. create table todos ( id integer primary key, user_id uuid not null, title text not null ); create policy "own rows" on todos for all to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id); -- migrations/0003_posts.sql: anyone reads, signed in or not. create table posts (id integer primary key, title text not null); create policy "read" on posts for select to anon, authenticated using (true); -- migrations/0004_reports.sql: an app only its admins sign in to. First run -- naro backend auth signups off and create the admin accounts (see Admin -- accounts below): authenticated then means exactly those accounts. create table reports (id integer primary key, body text not null); create policy "admins" on reports for all to authenticated using (true) with check (true); ``` - A migration or a query takes `CREATE POLICY`; `DROP POLICY [IF EXISTS] p ON t` (IF EXISTS skips a missing table too, as in Postgres); `COMMENT ON POLICY p ON t IS 'naro:owner '`, which makes p an owner rule like `naro policy set --owner` (any other comment is ignored); and `ALTER TABLE t ENABLE | FORCE | NO FORCE ROW LEVEL SECURITY`, which changes nothing because row-level security is always on. In a policy statement `public.todos` reads as `todos`. - Refused before anything runs, with `400 VALIDATION_FAILED` "Statement of : …" (a syntax error ends "at character ."): `ALTER TABLE … DISABLE ROW LEVEL SECURITY` (it is always on; to let everyone through write `CREATE POLICY "open" ON t USING (true) WITH CHECK (true);`), `GRANT` and `REVOKE` (every table is behind row-level security, so write a policy instead), `ALTER POLICY` (`DROP POLICY` and `CREATE POLICY` again), a table named `public.x` in `CREATE TABLE`, `DROP TABLE` or a rename — 'SQLite has no schema "public": write the table name without "public."' — and a `CREATE POLICY` with a column inside a subquery that has no table in front of it, when a later statement of the same request creates, drops, renames or alters the policy's table or a table its subqueries read: "a later statement changes a table, so name the table of every column inside this policy's subqueries (for example members.user_id)." (Postgres decides whose column it is when the policy is created; naro binds it once the request is in, when the later statement may have added it to the subquery's table. A later statement that changes only other tables leaves the policy alone.) - All the statements of one request apply all-or-nothing, the policies with the tables. A policy on a table that does not exist, a name already taken on that table or a 33rd policy on one table fails the whole request with `400 DATABASE_QUERY_FAILED` and `statementIndex` (the statement's number from 0): "statement 2 of 2 failed and the migration was rolled back: relation "nope" does not exist" (a query says "statement of failed, so none of the statements ran: …"). The other reasons are `policy "

" for table "" already exists`, `policy "

" for table "" does not exist` (DROP POLICY without IF EXISTS) and `A table may have at most 32 policies.` Policy statements, and statements that create, drop or rename a table, take no `params`: `400 VALIDATION_FAILED` "Policy statements cannot take params: write the values into the SQL." — for a table statement "Statements that create, drop or rename a table cannot take params: write the values into the SQL." - Policies follow their table, as in Postgres: `DROP TABLE` drops the table's policies, and a new table never inherits the leftovers of an earlier table of the same name (except tables changed through the table editor on a project without a backend). SQLite changes a column by rebuilding the table — `create table __new_x`, copy the rows, `drop table x`, `alter table __new_x rename to x` — so the `drop table x` removes x's policies and the rename does not bring them back: write the `create policy` statements again after the rename, in the same migration. A plain `alter table a rename to b` does not move them either: they stay under a, and b has none until you create them again. The answer's `warnings` names the policies a drop or a rebuild removed and has their SQL, ready to paste. Policies an earlier table left under a name, which a new table of that name cleared, are named without SQL: "Removed policies

left under by an earlier table of that name; the new starts with no policies — do not re-create them unless they fit the new table." - The answer carries `warnings`: a migration's as a top-level `warnings` array, a query's as `result.warnings` when there are any (MCP returns them as they are; the CLI prints each as `⚠ …` on stderr, `--json` has it whole). They say that the project has no backend yet — "Policies are saved and apply once the project backend is on (naro backend enable)." — which policies a drop or a rebuild removed, a policy that was applied but that the Data API refuses, with the reason, a number column a policy reads as a boolean whose DEFAULT is text, and the rows of such a column that already hold text when a new policy starts to read it ("notes has 2 rows whose is_private is text, …: update them to numbers."). - `naro db push --dry-run` parses the policy statements of every pending file on your machine and prints each refusal as `: Statement of : …` (exit 1); whether a table or a policy exists is known only when the file applies. `naro db pull` writes the policies at the end of the baseline file: `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` for each table, then each `CREATE POLICY` (an owner rule's column as `COMMENT ON POLICY … IS 'naro:owner '`); a database without policies pulls as plain SQLite. `naro db reset` clears the policies with the schema and creates them again from your migration files, so a policy made only with `naro policy create` or `naro policy set` is gone. Before it asks, it lists the policies your files do not make (`.`); to keep them, copy the SQL `naro db reset` prints for them (`policiesNotInFiles[].sql` in `--json`) into a migration first, which keeps owner rules (`COMMENT ON POLICY … 'naro:owner c'`); `naro policy list` prints owner rules only as a comment. The policies of a table your files do not create go with it and are named apart, without SQL (`policiesOfTablesNotInFiles`). It refuses to start, before it drops anything, when the control plane would refuse one of the files (as `naro db push --dry-run` checks them; `--dry-run` exits 1). `naro db lint` on a project with a backend adds `rls_no_policy` (warn: a table the Data API refuses entirely), `rls_policy_invalid` (error: a policy the worker cannot apply, such as one that reads a text column as true or false), `rls_policy_text_default` (warn: a number column a policy reads as a boolean or compares with a number whose DEFAULT SQLite keeps as text, such as `BOOLEAN DEFAULT 'true'` — every row there when the column was added holds that text) and `rls_policy_unindexed` (warn). - Policies are Postgres's row-level security, as in Supabase, and always on: default deny — a table without a policy refuses every anon and signed-in request; only the service key gets through. Write them as CREATE POLICY: `AS PERMISSIVE|RESTRICTIVE`, `FOR ALL|SELECT|INSERT|UPDATE|DELETE`, `TO anon|authenticated|public`, `USING (…)`, `WITH CHECK (…)`, with Postgres's clause rules (INSERT takes only WITH CHECK, SELECT and DELETE only USING). A request passes when one permissive policy for its command and role passes and every restrictive one does. select reaches rows the select policies pass; insert rows must pass WITH CHECK (and select's USING with `.select()`); update reaches rows its USING and select's USING pass, and each new row must pass WITH CHECK (USING when absent) and select's USING — a soft delete that hides the row is refused; delete reaches rows its USING and select's USING pass. A row outside USING is skipped silently; a written row that fails a check writes nothing and answers `403 PERMISSION_DENIED` "New row violates row-level security policy for table
." (a failed restrictive policy is named in quotes after "policy"). No permissive policy for the command is `403 PERMISSION_DENIED` "This request is not allowed by the policy of
." (Postgres would return no rows). - Expressions: columns, literals, comparisons, AND/OR/NOT, IS [NOT] NULL, IS [NOT] DISTINCT FROM, [NOT] IN, [NOT] LIKE/ILIKE, BETWEEN, `EXISTS (SELECT …)`, `IN (SELECT …)`, lower/upper/coalesce/length/now(), `auth.uid()`, `auth.role()`, `auth.jwt() ->> 'sub'|'role'|'email'|'email_verified'`; `(select auth.uid())` works. A subquery reads its table through that table's select policies, so give it one (e.g. members readable by its own user). Policies that expand into themselves are refused: "Infinite recursion detected in policy for relation
". The policies of one statement bind at most 50 values and write at most 50,000 bytes of SQL. A LIKE or ILIKE pattern is at most 50 bytes of UTF-8 (D1's limit; LIKE counts each *, ? and [ as 3). For anon, `auth.uid()` is NULL and `auth.jwt()` holds only `role: 'anon'`, as in Supabase, read with Postgres's NULL logic: `auth.uid() IS NULL` is true for anon, `user_id = auth.uid()` and its NOT match nothing; target anon with `TO anon` or `auth.role() = 'anon'`. `auth.jwt() ->> 'email'` is the address only once it is verified (NULL before: sign-up signs in unverified users); check `email_verified`, which is the text 'true' or 'false': `(auth.jwt() ->> 'email_verified') = 'true'`. IS TRUE, IS FALSE, NOT x and a value alone read a number or boolean column; on a claim, auth.uid(), a string, a function or a TEXT column they are refused (as text, 'true' would read as false). So is comparing text with TRUE or FALSE (<> false, = true, IN (true), BETWEEN, IS DISTINCT FROM, also through coalesce): the text 'true' is not TRUE, so <> false would hold for everyone. Comparisons follow SQLite affinity and collation; lower/upper/ILIKE fold ASCII only; now() is UTC text. - A subquery that reads the outer row runs once per row, so it needs an index: the control plane refuses the policy until a column its WHERE compares with = (or IN) to a value from outside starts an index, naming it (`naro db index create members team_id`); `naro policy create --allow-unindexed` (`?allow_unindexed=1`) accepts a full scan per row. - A check reads the row as it will be stored and compares with the column's collation (NOCASE, RTRIM): send numbers, not strings, to number columns it reads (no LIKE or text functions on them); always send a primary key it reads, not as null; send any other column it reads unless its default is none, NULL, TRUE, FALSE, a number or a quoted string on a non-number column. It cannot read generated or virtual-table columns. The Data API refuses a string for a column that any policy (of any table) reads, an owner rule's own id column excepted: `400 INVALID_QUERY` " must be a number, a boolean or null, since the policy checks it". For a column declared with INT, REAL, FLOA, DOUB, NUM, DEC, BOOL or BIT in its type that is every string — and even with no policy reading the column, a string SQLite would keep as text (`'abc'`, `'true'`; `'7'` is stored as 7): " must be a number, a boolean or null, since the column is declared ", as Postgres refuses it as a type error. That follows SQLite's own type rule, so Postgres types whose name holds INT or BIT (INTERVAL, POINT, VARBIT) are number columns here too. For another declared type (uuid, date, timestamptz, json…) it is only a string SQLite would store as a number (`'12'`), a string with a NUL character, and any string when a policy reads the column as true or false or compares it with a number — so Supabase's `user_id uuid` with `auth.uid() = user_id` works. An insert that leaves out such a column whose DEFAULT is a string SQLite keeps as text (`is_private BOOLEAN DEFAULT 'true'`, and a name SQLite makes a string: `DEFAULT "true"` or a bare word like `DEFAULT yes`) or a blob (`x'…'`) is refused too: `400 INVALID_QUERY` " must include , which the policy checks" — send the value. Two limits on such a uuid-like column: a WITH CHECK that orders it (`<`, `<=`, `>`, `>=`, BETWEEN) against a number-like value can judge a new text differently from the row it becomes, so keep ordering for number columns; and LIKE, ILIKE or a text function on it is refused as on a number column (`500 POLICY_INVALID`). `500 POLICY_INVALID` "Policy on
cannot be applied." means a stored policy names a column or table that is gone — drop it or create it again. - `naro policy set` is shorthand for the policies naro_select … naro_delete, for when you would rather not write them as SQL: each of select, insert, update and delete takes none, public, authenticated or owner. owner needs --owner : signed-in users reach only rows whose column holds their user id, and naro fills it in on insert (a naro extension; `policy list` prints it as a `-- naro: owner` comment, and in a migration `COMMENT ON POLICY p ON t IS 'naro:owner '` marks a policy of that shape — permissive, TO authenticated, one command, ` = auth.uid()` its only expression — as the owner rule; otherwise `400 DATABASE_QUERY_FAILED` says naro:owner needs that shape). Or a condition in the `.or()` filter syntax (commas are AND) with `auth.uid()` for the signed-in user's id: `--select-when`, `--delete-when`, `--insert-when`, `--update-when` plus `--update-check` (defaults to `--update-when`), `--signed-in` to refuse anon. For anon, one comparison with auth.uid() matches no row, nor does its not; inside a group SQL's three-valued logic applies, so `not(and(is_public.eq.1,user_id.eq.auth.uid()))` matches every non-public row for anon — write what a condition lets in. An `--update-check` replaces the default, so put `user_id.eq.auth.uid()` in it or users can give rows away. `--from-file policy.json` takes the JSON form, where auth.uid() is `{"auth":"uid"}`. - A backend turned on before named policies answers the commands that write a policy — `naro policy create`, `naro policy drop` and `naro policy set` — with `409 BACKEND_OUTDATED` "The project backend is older than named policies. Run `naro backend enable` to update it, then try again." `naro backend enable` uploads the current backend and turns the stored rules into named policies; it also turns a backend that is off back on, so run `naro backend disable` again afterwards if it should stay off. `naro policy list` and `naro policy remove` work in the meantime. A migration or query that holds a `CREATE POLICY`, a `DROP POLICY` or a `COMMENT ON POLICY … IS 'naro:owner …'` gets the same answer before anything runs; SQL without one runs as before. - `backend enable` answers 200 even when a step failed; the CLI exits 1 with `BACKEND_PROVISION_FAILED` and the step. Running it again resumes there. `backend status` calls a run that died part-way (`provisioning` with no live lease, JSON `stalled: true`) stalled; enable picks it up too. - `backend disable` turns it off: in up to about 90 seconds the app's `/_naro/*` requests get `404 NOT_FOUND` (the routing cache ages out: 30 s, plus up to 60 s for the change to reach every location). D1 with its data and users, the keys, sign-in settings and policies are kept; `backend enable` brings it back with the same keys, so the app's anon key keeps working. It asks first; without a terminal it needs `--yes` (`CONFIRMATION_REQUIRED`, exit 1). `backend status` prints `Backend: off since
.sql`. A column is `name:type[:modifier…]` — types INTEGER, REAL, TEXT, BLOB, NUMERIC, BOOLEAN, DATETIME, JSON (keep JSON in TEXT); modifiers pk, autoincrement, notnull, unique, default=, references=
., ondelete=. Dropping a table or column needs `--yes` (MCP: `confirm` = the table, or `
.`), or it answers `CONFIRMATION_REQUIRED`. SQLite cannot add a PRIMARY KEY/UNIQUE/ NOT NULL-without-default column, or drop a primary key, UNIQUE or indexed column; naro refuses those up front. Changing a column's type needs a table rebuild — write that migration yourself. `--owner user_id` adds the column and, with a ready backend, an owner policy; any other new table is default deny for the Data API until it has a policy (`CREATE POLICY` in a migration, or `naro policy create` or `naro policy set`). Policies follow table names, not tables: a drop removes them while the project has a backend. A rename leaves them under the old name, where they apply to no table (naro warns, with the commands that move them) — and the next table `naro db table`, the table editor or MCP creates as that name or renames to it, the same table renamed back included, has them removed first; a table made by `naro db push`, `naro db query`, `apply_migration` or `execute_sql` does not inherit them either. The answer's `removedPolicies` carries each removed policy as `CREATE POLICY` (`statements[].sql`; `sql` joins them with line breaks), which `naro policy create` takes back — except an owner rule (`statements[].ownerColumn` is set): CREATE POLICY has no owner column, so a policy made from that SQL refuses the insert that leaves the column out. `setCommand` is the `naro policy set
… --owner ` that sets those again. The CLI prints a statement that holds a control character or a line break as a `--` comment; `--json` has the exact text. A rename or drop that takes a table or column from under a policy — its own table's or, through a subquery, another's — makes the requests that policy applies to answer `500 POLICY_INVALID`: `warnings` names each policy the change breaks and why, in a dry run too. Preview destructive changes with `--dry-run` (MCP: `dryRun: true`) and read `warnings` first. - A migration request can time out (45s) after it already ran but before its ledger row was recorded. Run `naro migration list` before retrying — re-applying an already-applied migration re-runs its SQL. - An export (`naro db dump`, `export_database`) runs at Cloudflare and blocks every other query to the database until it finishes, so live traffic waits. Its download link works for about an hour for anyone who has it — never paste it anywhere public. While the database has virtual tables (FTS5), Cloudflare refuses a full or schema-only export; exporting only the other tables (--table) still works. - `naro db lint` changes nothing but needs database **write** access: its two checks (`foreign_key_check`, `quick_check`) scan the whole database, and naro classifies a whole-database scan as a write so a read-only role cannot start one. On a large database the scan costs rows read and can hit the 30-second query limit. - `naro db reset` drops every table, view and trigger the project owns, empties the ledger and re-applies ./migrations. The database itself is never deleted, and the backend's accounts, sessions and settings (the `_naro_` tables) are kept — except its policies: they are cleared and the migration files create them again. Before it drops anything it lists the policies your files do not make, each with the SQL that makes it again — its `CREATE POLICY` and, for an owner rule, the `COMMENT ON POLICY … IS 'naro:owner '` that keeps its column (`--json`: `policiesNotInFiles[].sql`); to keep them, add it to a migration first. A policy row that cannot be read is listed as a `-- Policy … cannot be read` comment instead: copying it keeps nothing, so drop that policy and create it again. And a backend whose worker predates named policies is refused with `409 BACKEND_OUTDATED` (`naro backend enable` first). It prints the bookmark from just before the drops — `naro db restore --bookmark ` undoes it. - `naro migration repair --status applied|reverted` edits the `d1_migrations` ledger and never runs the migration's SQL. Use it when a migration ran but its ledger row was not recorded. - A restore (`naro db restore`, `restore_database`) rolls back every write after the chosen point, the migration ledger included, and cancels queries in flight. It can be undone, though: restoring to the previousBookmark it returns undoes it.