@xano/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +788 -0
- package/LICENSE +21 -0
- package/README.md +541 -0
- package/dist/.build-fingerprint +1 -0
- package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
- package/dist/answer-shape-K67TAAPY.js +33 -0
- package/dist/backend-dir-2U53CIAL.js +39 -0
- package/dist/backend-slot-7QXW2QYC.js +28 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +255 -0
- package/dist/branch-NCRNWQBQ.js +61 -0
- package/dist/branch-commands-ACIRCEQJ.js +79 -0
- package/dist/bundle.d.ts +223 -0
- package/dist/bundle.js +153 -0
- package/dist/capture-WAXPD7DR.js +41 -0
- package/dist/chunk-27GWNP2I.js +23 -0
- package/dist/chunk-2CMJZDCL.js +177 -0
- package/dist/chunk-2EZ3ZJZZ.js +302 -0
- package/dist/chunk-2GNFDZA2.js +390 -0
- package/dist/chunk-2MNLRIOX.js +11 -0
- package/dist/chunk-2TFRR6JV.js +104 -0
- package/dist/chunk-2ZUEE4EK.js +408 -0
- package/dist/chunk-36JUQZVQ.js +22 -0
- package/dist/chunk-3DRXWLJ5.js +83 -0
- package/dist/chunk-3JDU4VRL.js +137 -0
- package/dist/chunk-3KZCLN4Z.js +241 -0
- package/dist/chunk-3LDULPRE.js +168 -0
- package/dist/chunk-3M4LFYNQ.js +111 -0
- package/dist/chunk-3XI5CTUE.js +22 -0
- package/dist/chunk-3YW23UFH.js +50 -0
- package/dist/chunk-45UIT5T2.js +862 -0
- package/dist/chunk-4BW2SG2N.js +46 -0
- package/dist/chunk-4KHEMC32.js +79 -0
- package/dist/chunk-4POWHT2K.js +321 -0
- package/dist/chunk-4Q2GN7NW.js +192 -0
- package/dist/chunk-5A2PLKZB.js +70 -0
- package/dist/chunk-5FBYXK5P.js +159 -0
- package/dist/chunk-5HVXPTU4.js +323 -0
- package/dist/chunk-5QU2P5CA.js +304 -0
- package/dist/chunk-5V5XU32D.js +191 -0
- package/dist/chunk-5ZSO4T2G.js +10 -0
- package/dist/chunk-622HUCZT.js +10 -0
- package/dist/chunk-65HWOMGV.js +177 -0
- package/dist/chunk-6ELFRN3P.js +365 -0
- package/dist/chunk-6HO3NCSJ.js +1055 -0
- package/dist/chunk-6IUMKYWI.js +457 -0
- package/dist/chunk-6KEB3C3U.js +65 -0
- package/dist/chunk-6MR5TI5V.js +987 -0
- package/dist/chunk-6NNTNXN7.js +157 -0
- package/dist/chunk-6PBR37LN.js +97 -0
- package/dist/chunk-6QXLMMT3.js +155 -0
- package/dist/chunk-6ZGJCBZS.js +224 -0
- package/dist/chunk-73IVBKMA.js +250 -0
- package/dist/chunk-7G3CTU7I.js +286 -0
- package/dist/chunk-A3UDOZB6.js +222 -0
- package/dist/chunk-A6E77GDW.js +85 -0
- package/dist/chunk-AHJWCYQO.js +660 -0
- package/dist/chunk-AKNCFVKZ.js +36 -0
- package/dist/chunk-AM4FKL4J.js +237 -0
- package/dist/chunk-AN6BL47T.js +26 -0
- package/dist/chunk-AOSYVMUS.js +147 -0
- package/dist/chunk-APWXY5SL.js +182 -0
- package/dist/chunk-AWEIIR2U.js +218 -0
- package/dist/chunk-AWQ7BQTQ.js +73 -0
- package/dist/chunk-BAUZOWGX.js +393 -0
- package/dist/chunk-BHACRS6R.js +940 -0
- package/dist/chunk-BWPNT55T.js +680 -0
- package/dist/chunk-C6RIMRU4.js +187 -0
- package/dist/chunk-CGVQZXYG.js +263 -0
- package/dist/chunk-CIMQZRRE.js +162 -0
- package/dist/chunk-CJHQKJU4.js +113 -0
- package/dist/chunk-CTMO46PK.js +1897 -0
- package/dist/chunk-D53J55C5.js +106 -0
- package/dist/chunk-D7VGG3WS.js +90 -0
- package/dist/chunk-DAD3ONIS.js +245 -0
- package/dist/chunk-DGWHAVRG.js +162 -0
- package/dist/chunk-DX4QR4B2.js +483 -0
- package/dist/chunk-EHP3WPEG.js +21 -0
- package/dist/chunk-ELP2HMXC.js +432 -0
- package/dist/chunk-ERVIASGL.js +21 -0
- package/dist/chunk-ET4Y4XCB.js +865 -0
- package/dist/chunk-ETO6LGLD.js +4750 -0
- package/dist/chunk-EUUUMY7J.js +24 -0
- package/dist/chunk-FQJ2TTHP.js +218 -0
- package/dist/chunk-FQOF7E2E.js +133 -0
- package/dist/chunk-FWD7AL6N.js +21 -0
- package/dist/chunk-FXFV3KU6.js +138 -0
- package/dist/chunk-FXRQOWTE.js +171 -0
- package/dist/chunk-G554NZNI.js +210 -0
- package/dist/chunk-GSGDWJB5.js +13 -0
- package/dist/chunk-H2V3N23D.js +66 -0
- package/dist/chunk-H5C7MBCR.js +159 -0
- package/dist/chunk-HHN734BU.js +371 -0
- package/dist/chunk-HRVBLCVJ.js +243 -0
- package/dist/chunk-HVRUQXGD.js +1264 -0
- package/dist/chunk-I4F5TCJH.js +130 -0
- package/dist/chunk-IDOTCMDV.js +57 -0
- package/dist/chunk-IGOLHKWF.js +427 -0
- package/dist/chunk-JGE7CAWB.js +148 -0
- package/dist/chunk-JTUKLCXS.js +92 -0
- package/dist/chunk-JZGVVJSI.js +66 -0
- package/dist/chunk-K4WX6UUU.js +923 -0
- package/dist/chunk-K5BBGTYZ.js +217 -0
- package/dist/chunk-K6RXBGUB.js +427 -0
- package/dist/chunk-KTSDSD36.js +165 -0
- package/dist/chunk-L5JYN3NM.js +1038 -0
- package/dist/chunk-LIEOKXKD.js +354 -0
- package/dist/chunk-LLNZGEFK.js +51 -0
- package/dist/chunk-LPECSJ2J.js +55 -0
- package/dist/chunk-LQCMKAJ7.js +413 -0
- package/dist/chunk-LRN4ZJCQ.js +1145 -0
- package/dist/chunk-MJ4ZVHKN.js +17 -0
- package/dist/chunk-MXHSQOC4.js +3061 -0
- package/dist/chunk-N5GKCWE5.js +367 -0
- package/dist/chunk-NA3PXMEF.js +10 -0
- package/dist/chunk-NAQDMEHV.js +76 -0
- package/dist/chunk-NWKXM5RD.js +109 -0
- package/dist/chunk-NZOWWHU5.js +196 -0
- package/dist/chunk-OHX6MIUZ.js +184 -0
- package/dist/chunk-OOXWYAA4.js +15 -0
- package/dist/chunk-OPHB5ORL.js +1990 -0
- package/dist/chunk-OXA6PSZT.js +1725 -0
- package/dist/chunk-P2HAGF6H.js +73 -0
- package/dist/chunk-PTHVMXZI.js +3036 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q325JKYB.js +189 -0
- package/dist/chunk-Q7DGTQ5G.js +255 -0
- package/dist/chunk-QCNYHBL7.js +601 -0
- package/dist/chunk-QO6BVGS5.js +145 -0
- package/dist/chunk-QXTKSOFI.js +76 -0
- package/dist/chunk-ROY4YAEQ.js +66 -0
- package/dist/chunk-RTJRZFFY.js +447 -0
- package/dist/chunk-SAVJUS2X.js +128 -0
- package/dist/chunk-SFLHVCKX.js +173 -0
- package/dist/chunk-STSFNCY4.js +127 -0
- package/dist/chunk-SWBF2LTK.js +170 -0
- package/dist/chunk-T5KELPAR.js +109 -0
- package/dist/chunk-TFFGOFVZ.js +1481 -0
- package/dist/chunk-TLHIFK4M.js +256 -0
- package/dist/chunk-TLQE7ZN3.js +156 -0
- package/dist/chunk-TPHWJ7UG.js +1981 -0
- package/dist/chunk-TRJCALW3.js +291 -0
- package/dist/chunk-TTEIYD5P.js +133 -0
- package/dist/chunk-U64O2MXY.js +407 -0
- package/dist/chunk-UCQ2VUTG.js +630 -0
- package/dist/chunk-UTI5G6OJ.js +44 -0
- package/dist/chunk-UVHDMMIP.js +188 -0
- package/dist/chunk-UZ6QQWJT.js +120 -0
- package/dist/chunk-VAX3UZOY.js +686 -0
- package/dist/chunk-W3HBUKFO.js +57 -0
- package/dist/chunk-WKQLGKCL.js +2823 -0
- package/dist/chunk-WQFNASK4.js +560 -0
- package/dist/chunk-WRBKLYSJ.js +224 -0
- package/dist/chunk-WTPZRR4I.js +17 -0
- package/dist/chunk-WZDJ2LLX.js +2385 -0
- package/dist/chunk-X2NL4KUO.js +14 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-X677DMZM.js +7050 -0
- package/dist/chunk-XBKMWY7F.js +612 -0
- package/dist/chunk-XNCP4KYO.js +78 -0
- package/dist/chunk-XVBGLISP.js +35 -0
- package/dist/chunk-XW6KISBB.js +1192 -0
- package/dist/chunk-YGZHHAAB.js +8156 -0
- package/dist/chunk-YHEFQUQB.js +186 -0
- package/dist/chunk-YLDALTWZ.js +653 -0
- package/dist/chunk-YU4HSQW7.js +229 -0
- package/dist/chunk-ZDPP6RKB.js +27 -0
- package/dist/chunk-ZFDZKNGG.js +194 -0
- package/dist/chunk-ZIY7RNUT.js +49 -0
- package/dist/chunk-ZYJP6BOO.js +30 -0
- package/dist/cli.d.ts +1111 -0
- package/dist/cli.js +165 -0
- package/dist/codegen-command-VR2GWR6C.js +173 -0
- package/dist/codegen.d.ts +115 -0
- package/dist/codegen.js +19 -0
- package/dist/commands-GJMJ4XYZ.js +76 -0
- package/dist/completion-ZQLO6PB6.js +738 -0
- package/dist/config-MBYGCAFK.js +136 -0
- package/dist/deploy-command-JVZPWHMK.js +180 -0
- package/dist/deploy-source-3W45CXMP.js +81 -0
- package/dist/dev-env-sync-HN44K4D5.js +188 -0
- package/dist/diagnostics-UEAAHWVF.js +30 -0
- package/dist/display-name-hint-ABBPVQIR.js +46 -0
- package/dist/emit-CyhBDxli.d.ts +7007 -0
- package/dist/env-example-refresh-ET5HMMDW.js +126 -0
- package/dist/env-pull-command-TNMNB3CV.js +323 -0
- package/dist/env-target-O4HRKTGZ.js +40 -0
- package/dist/env-var-command-ZIRDCAZA.js +427 -0
- package/dist/ephemeral-P7F3PMAD.js +54 -0
- package/dist/ephemeral-command-CLJJKOFT.js +592 -0
- package/dist/ephemeral-state-PYCBSOGX.js +61 -0
- package/dist/file-lock-PT74GWYW.js +10 -0
- package/dist/generate-command-AYEOLA26.js +510 -0
- package/dist/gitignore-3SJ3WNRG.js +51 -0
- package/dist/guards-4EBNCZDC.js +162 -0
- package/dist/hosted-file-PSHJB3HX.js +28 -0
- package/dist/impersonate-command-5JAS3WSV.js +152 -0
- package/dist/import-EQSFU6JC.js +19 -0
- package/dist/index.d.ts +5470 -0
- package/dist/index.js +371 -0
- package/dist/init-command-X2FR4UAK.js +125 -0
- package/dist/init-modules-RY7UHAOL.js +50 -0
- package/dist/init-web-M4YZSZK6.js +42 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +2634 -0
- package/dist/io-RC56QSN5.js +26 -0
- package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
- package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
- package/dist/landed-753YLJ5H.js +40 -0
- package/dist/landing-record-SU52SKXU.js +61 -0
- package/dist/live-diff-YONW2VI3.js +65 -0
- package/dist/local-engine-choice-CH5X3W7E.js +79 -0
- package/dist/local-engine-command-Z3OL7VOD.js +766 -0
- package/dist/local-engine-config-5U3SC434.js +39 -0
- package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
- package/dist/local-engine-process-EHTE5LLU.js +63 -0
- package/dist/local-engine-state-A4FTRFAF.js +32 -0
- package/dist/local-engine-update-check-6L645VBL.js +162 -0
- package/dist/lock-MRRHW5GS.js +87 -0
- package/dist/lock-commands-2QG25HL7.js +963 -0
- package/dist/login-command-QPYRWOXZ.js +538 -0
- package/dist/logout-command-WOIQLDP6.js +56 -0
- package/dist/loop-ALIZLOSX.js +191 -0
- package/dist/marketplace-command-K5YL42NO.js +977 -0
- package/dist/marketplace-resolve-LY32JES5.js +40 -0
- package/dist/meta-client-YXGPW53C.js +244 -0
- package/dist/module-manifest-JFGHU2TC.js +38 -0
- package/dist/near-key.d.ts +13 -0
- package/dist/near-key.js +8 -0
- package/dist/node.d.ts +95 -0
- package/dist/node.js +969 -0
- package/dist/operation-outcome-TZSESNC7.js +51 -0
- package/dist/output-target-AL3I44GG.js +47 -0
- package/dist/package-manager-PRAHUSCN.js +34 -0
- package/dist/plan-disclosure-6G4MLZKD.js +114 -0
- package/dist/plugin-contributions-WRNY5ABG.js +28 -0
- package/dist/plugin-prompt-4VA3B4FF.js +20 -0
- package/dist/plugin-questions-JTSYA74Q.js +24 -0
- package/dist/plugin.d.ts +396 -0
- package/dist/plugin.js +1 -0
- package/dist/preflight-command-2T5Z36MI.js +321 -0
- package/dist/profile-command-AH3PIM4X.js +800 -0
- package/dist/profile-pointer-GRWJU3YD.js +31 -0
- package/dist/profile-select-74TRDEPD.js +23 -0
- package/dist/project-dependency-KSH63ODT.js +35 -0
- package/dist/project-sdk-REPPOIBD.js +24 -0
- package/dist/promote-command-PCWT5N6S.js +140 -0
- package/dist/prompt-II3F3LND.js +27 -0
- package/dist/publish-command-TTDUUJI5.js +414 -0
- package/dist/pull-command-VBO7C4S3.js +161 -0
- package/dist/pull-superseded-GJWLG7XO.js +20 -0
- package/dist/release-command-N4ZPKMHR.js +146 -0
- package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
- package/dist/response-HlXahv4K.d.ts +925 -0
- package/dist/retry-command-AS5EVREX.js +34 -0
- package/dist/routes-manifest-SNWUVBT6.js +43 -0
- package/dist/runtime-XETHVF33.js +14 -0
- package/dist/scaffold.d.ts +1911 -0
- package/dist/scaffold.js +107 -0
- package/dist/secrets-file-LVZD6IIM.js +56 -0
- package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
- package/dist/seed-VU7C3QYS.js +46 -0
- package/dist/sent-writes-N7X4OVXT.js +22 -0
- package/dist/source-resolve-S2XA3XZF.js +83 -0
- package/dist/static-host-B2Y6AG3W.js +352 -0
- package/dist/status-command-RUDXTF7I.js +554 -0
- package/dist/store-6VNCD4D7.js +20 -0
- package/dist/store-ARM6VMO5.js +98 -0
- package/dist/store-DuYfzKlW.d.ts +3144 -0
- package/dist/suggest-6FOOYKHG.js +12 -0
- package/dist/sveltekit.d.ts +154 -0
- package/dist/sveltekit.js +109 -0
- package/dist/sync-contributions-UCEROO26.js +415 -0
- package/dist/table-ENAVMITR.js +42 -0
- package/dist/tables-command-GUPXXSB5.js +156 -0
- package/dist/tenant-IIHPOHGP.js +42 -0
- package/dist/tenant-command-D5HGI2X4.js +965 -0
- package/dist/test-command-TIKZMDPZ.js +476 -0
- package/dist/tests-BGGIOK3L.js +26 -0
- package/dist/token-NICJUTMT.js +72 -0
- package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
- package/dist/toolchain-modules-5EAZUONB.js +24 -0
- package/dist/tracked-backend-EIBUMDZA.js +80 -0
- package/dist/update-check-2W2NLDTC.js +136 -0
- package/dist/upgrade-command-QHNFWM7O.js +531 -0
- package/dist/verify-rollout-DRCK5TDD.js +61 -0
- package/dist/whoami-command-STGINTXH.js +51 -0
- package/dist/workspace-2CLHU2LK.js +93 -0
- package/dist/workspace-binding-56PIIPCP.js +54 -0
- package/dist/workspace-command-JEXQETJD.js +535 -0
- package/dist/workspace-env-F2RFMSC5.js +70 -0
- package/dist/workspace-export-OJSH2HIW.js +21 -0
- package/dist/writable-VA7OISSK.js +25 -0
- package/dist/xano-ICFUSPFR.js +42 -0
- package/dist/xanosdk-import-GN42TO2B.js +56 -0
- package/dist/xanosdk-project-S6DXJONQ.js +41 -0
- package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
- package/guides/README.md +33 -0
- package/guides/authoring.md +921 -0
- package/guides/cli.md +401 -0
- package/guides/codegen.md +177 -0
- package/guides/codes.md +404 -0
- package/guides/coverage.md +74 -0
- package/guides/deploying.md +1525 -0
- package/guides/environment.md +400 -0
- package/guides/marketplace.md +415 -0
- package/guides/module-contract.md +558 -0
- package/guides/object-kinds.md +433 -0
- package/guides/project-structure.md +43 -0
- package/guides/scaffold.md +351 -0
- package/guides/typed-frontend.md +242 -0
- package/llms/client.md +63 -0
- package/llms/errors.md +56 -0
- package/llms/fields.md +176 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +30 -0
- package/llms/kinds-core.md +100 -0
- package/llms/kinds-knowledge.md +20 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +94 -0
- package/llms/legacy.md +44 -0
- package/llms/lock.md +40 -0
- package/llms/object-kinds.md +34 -0
- package/llms/statements-calls.md +62 -0
- package/llms/statements-catalog.md +320 -0
- package/llms/statements-data.md +66 -0
- package/llms/statements-runtime.md +68 -0
- package/llms/tests.md +59 -0
- package/llms/toolchain.md +16 -0
- package/llms/triggers.md +36 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +2085 -0
- package/llms.txt +493 -0
- package/manifest.json +11224 -0
- package/package.json +162 -0
- package/types/lambda-globals.d.ts +129 -0
|
@@ -0,0 +1,1525 @@
|
|
|
1
|
+
# Signing in & deploying
|
|
2
|
+
|
|
3
|
+
Authentication, ephemeral environments, static hosting, releasing to production, and validating against a live instance.
|
|
4
|
+
|
|
5
|
+
## Signing in & deploying (in depth)
|
|
6
|
+
|
|
7
|
+
**Sign in once** with `xanosdk login`. It runs the standard authorization-code + PKCE
|
|
8
|
+
browser flow (powered by the OpenID-certified [`openid-client`](https://github.com/panva/openid-client)):
|
|
9
|
+
opens your browser, you approve, and the CLI captures the redirect on a `127.0.0.1`
|
|
10
|
+
callback. On first use it dynamically registers its own OAuth client (RFC 7591) so the
|
|
11
|
+
authorize step never depends on the server tolerating an arbitrary loopback port; the
|
|
12
|
+
registration is cached in `~/.xano/xanosdk-clients.json`. The instance you're bound to is
|
|
13
|
+
read from the token's own `aud` claim.
|
|
14
|
+
|
|
15
|
+
`login` also **pins the numeric workspace** you consented to into the credential, so every
|
|
16
|
+
later command acts on exactly that workspace without looking it up again. There is **no
|
|
17
|
+
`--workspace` flag** — a credential addresses exactly one instance and one workspace. Run
|
|
18
|
+
`xanosdk workspace details` to see which.
|
|
19
|
+
|
|
20
|
+
The credential caches by default in a **shared** `~/.xanosdk/auth.json`, reusable
|
|
21
|
+
from **any** project directory — so a single `xanosdk login` covers all your projects.
|
|
22
|
+
Override the OAuth host with `--origin`/`$XANO_ORIGIN` and the loopback port with `--port`.
|
|
23
|
+
|
|
24
|
+
**Named profiles.** That file holds any number of credentials, each under a name.
|
|
25
|
+
`xanosdk login --profile staging` adds one without touching the others; `--profile <name>`
|
|
26
|
+
(or `-p`) selects one for a single command. See [Credential profiles](#credential-profiles)
|
|
27
|
+
below.
|
|
28
|
+
|
|
29
|
+
**When the browser can't reach this machine** — a remote shell, a container, a
|
|
30
|
+
Codespace — the `127.0.0.1` redirect lands nowhere and `login` waits five minutes for a
|
|
31
|
+
callback that will never arrive. Run `xanosdk login --paste` instead. It binds nothing:
|
|
32
|
+
it prints the authorize URL, you finish signing in in whatever browser you have, and
|
|
33
|
+
your browser then fails to load the `127.0.0.1` callback — which is expected. Paste that
|
|
34
|
+
URL back from the address bar (or just the `code=` value out of it) and the sign-in
|
|
35
|
+
completes. Everything else is identical, including the instance and workspace you pick
|
|
36
|
+
at consent.
|
|
37
|
+
|
|
38
|
+
Two things follow from how the code is bound. The paste has to happen in the same run
|
|
39
|
+
that printed the URL, so `--paste` needs a terminal — for automation use the environment
|
|
40
|
+
credential instead (see [Environment variables](environment.md)). And `--port` still
|
|
41
|
+
matters under `--paste`, not because anything binds it, but because it is part of the
|
|
42
|
+
redirect URL's identity.
|
|
43
|
+
|
|
44
|
+
Note this is **not** what `XANO_NO_BROWSER` does: that only suppresses the browser
|
|
45
|
+
launch, and still needs the redirect to arrive here.
|
|
46
|
+
|
|
47
|
+
Running `login` again when the **selected profile** already holds a usable credential
|
|
48
|
+
prints what you're signed in to and stops, rather than spending another browser round
|
|
49
|
+
trip — pass `--force` to sign in anyway. `login --profile staging` on a machine that
|
|
50
|
+
already has `prod` proceeds to consent: the guard is per profile, not per file.
|
|
51
|
+
|
|
52
|
+
## Credential profiles
|
|
53
|
+
|
|
54
|
+
`auth.json` holds a map of **named profiles** under a versioned envelope. Each profile is
|
|
55
|
+
one credential — one instance, one workspace — in one of two shapes:
|
|
56
|
+
|
|
57
|
+
- **`oauth`** — written by `xanosdk login`. Refreshes and rotates. Do not hand-edit.
|
|
58
|
+
- **`token`** — a meta API bearer token for the same meta APIs, for automation. No login
|
|
59
|
+
flow, no refresh, no rotation. Store one with `xanosdk profile add <name> --instance <url>
|
|
60
|
+
--workspace-id <n>`, which prompts for the token (or reads it piped on stdin) rather than
|
|
61
|
+
taking it as a flag (a secret on a command line lands in the process list and the shell
|
|
62
|
+
history), and checks it against the instance before storing it. The token
|
|
63
|
+
itself stays valid until you revoke it wherever you minted it — `xanosdk logout` and
|
|
64
|
+
`xanosdk profile delete` only remove the CLI's copy.
|
|
65
|
+
|
|
66
|
+
Both shapes work at **either** location (project-local `./.xano/auth.json` or global
|
|
67
|
+
`~/.xanosdk/auth.json`) on the same file precedence ladder below.
|
|
68
|
+
|
|
69
|
+
**Which profile a command acts as**, highest first:
|
|
70
|
+
|
|
71
|
+
1. `--profile <name>` / `-p <name>` on the command line.
|
|
72
|
+
2. `xano.profile.json` at the project root — a committed, secret-free pointer naming one
|
|
73
|
+
profile, found by walking up from the working directory and stopping at the first
|
|
74
|
+
`package.json`. Write it with `xanosdk profile use <name>`. Nothing ambient outranks it:
|
|
75
|
+
a `$XANO_PROFILE` left in a shell rc cannot retarget a pinned project.
|
|
76
|
+
3. `$XANO_PROFILE`.
|
|
77
|
+
4. The credential file's own `default` key. Set it with `xanosdk profile set-default <name>`.
|
|
78
|
+
5. The literal name `default`.
|
|
79
|
+
|
|
80
|
+
`xanosdk profile use` pins **this project**; `xanosdk profile set-default` changes **this
|
|
81
|
+
machine**. `xanosdk profile list` shows every profile with the default and the active one
|
|
82
|
+
marked, and `xanosdk whoami` and `xanosdk status` both report which profile is in play and
|
|
83
|
+
which rung chose it.
|
|
84
|
+
|
|
85
|
+
Writes are serialised on a lock and merge one profile at a time, so signing in, signing
|
|
86
|
+
out, or refreshing a token in one profile never touches another — including from two
|
|
87
|
+
processes at once.
|
|
88
|
+
|
|
89
|
+
> **Upgrading:** a file written before profiles is read as the `default` profile with no
|
|
90
|
+
> action needed. It is rewritten in the new shape the next time something writes to it.
|
|
91
|
+
|
|
92
|
+
**Project-local credentials** — pass `--local` to `login` to cache tokens in a
|
|
93
|
+
**project-local** `./.xano/auth.json` instead (which `login` **auto-adds to `.gitignore`**),
|
|
94
|
+
scoping the sign-in to that directory. Every command that **reads** credentials
|
|
95
|
+
(`deploy`/`details`, `whoami`, token refresh) resolves them **project-local first, global
|
|
96
|
+
as a fallback**: it uses `./.xano/auth.json` when present, otherwise `~/.xanosdk/auth.json` —
|
|
97
|
+
so a `--local` project keeps working without repeating the flag. `login` and `logout` do
|
|
98
|
+
**not** fall back: they target the shared global cache unless you pass `--local`. A `logout`
|
|
99
|
+
naming a profile that only `./.xano/auth.json` holds exits 8 (not found in the shared file)
|
|
100
|
+
and names `xanosdk logout --local -p <name>` — it does not report the session as gone. A
|
|
101
|
+
logout clears stored profiles only: with the `XANO_*` credential variables still set, it
|
|
102
|
+
says so, and commands keep authenticating with them. An explicit
|
|
103
|
+
`--config`/`$XANO_CONFIG` wins over both default file locations; only the three-variable
|
|
104
|
+
environment credential (below) outranks it, since it reads no file at all.
|
|
105
|
+
|
|
106
|
+
`deploy <file>` runs the exact same pipeline as `export` (including writing and
|
|
107
|
+
seeding `xano.lock` — the identities come from your code, so a throwaway
|
|
108
|
+
environment cannot leak into them), then create-or-refreshes the target environment and imports the compiled
|
|
109
|
+
workspace into it as a full replace — or, with `--keep-data`, as a merge (see
|
|
110
|
+
[Keeping your data between deploys](#keeping-your-data-between-deploys)). `deploy --bundle <path>` skips the compile and uploads a
|
|
111
|
+
bundle a previous `export` wrote (handy in CI). A **projected, secret-free summary** prints to stdout
|
|
112
|
+
as JSON, while the human-readable progress (and the live URLs) echoes to stderr:
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
{
|
|
116
|
+
"kind": "ephemeral", // or "local-engine"
|
|
117
|
+
"destination": { // the PARENT it was created under, then the environment itself
|
|
118
|
+
"instance": "https://x.xano.io", "workspaceId": 1, "kind": "ephemeral",
|
|
119
|
+
"label": "ezm0-xkdq-6564", "url": "https://x.xano.io/tenant/ezm0-xkdq-6564",
|
|
120
|
+
"display": "my-app" // its display name, when it has one other than the handle
|
|
121
|
+
},
|
|
122
|
+
"profile": { "name": "default", "source": "implicit" }, // null for an environment credential
|
|
123
|
+
"url": "https://x.xano.io/tenant/ezm0-xkdq-6564", // the environment's own base URL
|
|
124
|
+
"ephemeral": { "name": "ezm0-xkdq-6564", "display": "my-app", "expiresAt": "…" },
|
|
125
|
+
"created": true, // false on a refresh
|
|
126
|
+
"data": "replaced" // or "kept" — see Keeping your data between deploys
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Present only when they apply: `localEngine` (in place of `ephemeral`), `keepDataSkipped` and the
|
|
131
|
+
merge lists (see [Keeping your data between deploys](#keeping-your-data-between-deploys)),
|
|
132
|
+
`static` (`url`; `canonical`, the build's identity, and `verified` when the frontend was
|
|
133
|
+
confirmed live serving it; `staticEnv` — `{ injected, keys, documents, skipped?, reason? }` — when config was
|
|
134
|
+
injected; or `error` with `retry`, the `xanosdk publish` that retries only the static step),
|
|
135
|
+
`microservices` (or `microservicesError`, `{ message, remedy }`, when their status could not be
|
|
136
|
+
read after the deploy), `testRun` (`total` — the whole suite, `passed + failed + notRun` —
|
|
137
|
+
`passed`, `failed`, `notRun`, and `tests[]` each with `kind`,
|
|
138
|
+
`name`, `qualified` — the `type:object/name` form `xanosdk test run` takes (a query's object is its
|
|
139
|
+
lock key, `query:<group>|<verb>|<name>/<test>`), `workflow:<name>` for a workflow test — `object` for a
|
|
140
|
+
unit test (a query's carries `group` and `verb`), `status`, `message` (the first failure),
|
|
141
|
+
`expectations` for a unit test — every expectation in order, `{ index, status, message? }`, `index`
|
|
142
|
+
from 0 — and `timing` for a workflow test — the engine reports none for a unit test; plus `error` and `retry`, the `xanosdk test run-all` to run
|
|
143
|
+
again, when the suite could not be run — exit 6 — and `unreachable`, the tests it never reached
|
|
144
|
+
(as `test run` takes them), when it stopped answering part-way; the same `N passed, M failed,
|
|
145
|
+
K not run` line `test run-all` prints closes the run), `landingRecord` (`{ destination, file,
|
|
146
|
+
recorded, changed, cleared }`: what this deploy recorded as landed, the scope of a later
|
|
147
|
+
`--prune`; `null` for a local engine), `staticRemoved` (`{ urls, verified }`: the frontends a replace took down, below),
|
|
148
|
+
`notApplied` (flags accepted and not acted on — `--expires-hours` on an ephemeral that already
|
|
149
|
+
exists), `unconfigured`, and `passwordHashesUnverifiable` (below). The raw
|
|
150
|
+
workspace blob is deliberately never dumped: it carries per-tenant secrets that must not land
|
|
151
|
+
in shell history or CI logs.
|
|
152
|
+
|
|
153
|
+
`xanosdk test run-all --json` (and `test run`, `test list`) writes the same per-test entries at
|
|
154
|
+
the top level, beside the backend they ran on: `kind`, `env` (the ephemeral's or tenant's handle;
|
|
155
|
+
`null` for a local engine or a workspace), `name` (the backend's own name whatever its kind — the
|
|
156
|
+
same handle on an ephemeral or tenant, the engine's name on a local engine) and `display` (its display
|
|
157
|
+
name), each always present, `null` where the backend has none. When the suite stops answering
|
|
158
|
+
part-way, the rest are not started and the run exits 6 with a failure document whose
|
|
159
|
+
`details.results` is that document — `total` the whole suite, `notRun` and `unreachable` the tests
|
|
160
|
+
that could not be run, `tests[]` the ones that did — and `details.unreachable` names them again
|
|
161
|
+
(as `test run` takes them). Every run document carries `notRun` (`0` on a run that finished), the
|
|
162
|
+
same counts `deploy --test`'s `testRun` carries.
|
|
163
|
+
|
|
164
|
+
**A retry the error prints** — when a deploy stops short (a new ephemeral not ready yet) or its
|
|
165
|
+
import's answer is lost, the error names the command to run next: this run's own, with its
|
|
166
|
+
credential flags, without create-only flags like `--expires-hours` (the ephemeral already
|
|
167
|
+
exists), and with no secret in it. `--env-var`, `--doc-token` and `--origin` values are left
|
|
168
|
+
out and named (`--env-var GREETING`) — pass them again, or keep them in `xano/.env` and
|
|
169
|
+
`xano/.secrets.json`, which a deploy reads. The same text is the `--json` error's `message`.
|
|
170
|
+
|
|
171
|
+
**Where it goes** — the instance your **token is bound to** (the token's `aud`), never a
|
|
172
|
+
flag. `xanosdk deploy` create-or-refreshes an **ephemeral**, which is the only environment it
|
|
173
|
+
writes to. Reaching your real workspace is the separate release flow: cut a release from
|
|
174
|
+
what ran, then `xanosdk promote <release>` lands it there as a new branch, leaving live
|
|
175
|
+
untouched until you say otherwise.
|
|
176
|
+
|
|
177
|
+
**Static host** — `deploy --static <dir>` archives a directory and deploys it to a
|
|
178
|
+
static host after the backend import. The frontend lands **on the ephemeral itself**, so the
|
|
179
|
+
backend and the frontend live in one environment. The CLI uploads the archive to
|
|
180
|
+
`/api:meta/workspace/{id}/static_host/default/build` with your ordinary bearer. That route
|
|
181
|
+
auto-creates the `default` host (and only that one: a `--static-host` naming a host the target
|
|
182
|
+
does not have is not found, exit `8` from `publish`, naming the hosts it does have) and
|
|
183
|
+
**auto-deploys to `dev`**, returning the live URL — so
|
|
184
|
+
the static step is independent of the backend deploy (the backend still runs first because
|
|
185
|
+
it's the primary action). The directory is checked **before anything is uploaded**: a missing
|
|
186
|
+
path, a file, an empty directory, or a build whose compressed archive is over the 100 MB upload
|
|
187
|
+
cap is refused and nothing is deployed (`publish` checks the cap before it asks, too). An upload that fails
|
|
188
|
+
after the backend landed leaves the backend in place and still writes the deploy's own `--json`
|
|
189
|
+
summary (not a failure document), with `static` holding `error` (the reason, as printed),
|
|
190
|
+
`retry` (the `xanosdk publish` that retries only the static step) and `completed`. The exit
|
|
191
|
+
code says whether the frontend is up: `3` with `completed: "no"` when nothing was sent or the
|
|
192
|
+
host refused it, `9` with `completed: "unknown"` when the upload was sent and its answer lost
|
|
193
|
+
(a 5xx, a timeout, a dropped connection) — run `retry`, which settles it either way.
|
|
194
|
+
|
|
195
|
+
A **replacing** deploy clears the target's static hosting — whatever the flags, the replace
|
|
196
|
+
removes it — so a later plain `xanosdk deploy`, or `deploy --to … --replace` (a workspace, a
|
|
197
|
+
tenant, or an ephemeral named as one), takes a published frontend down. Before the import (and
|
|
198
|
+
in a `--dry-run` plan) it reads which frontends the target is serving from the platform — one
|
|
199
|
+
another project or the dashboard published included — warns naming each URL, and reports them
|
|
200
|
+
as `staticRemoved: { urls, verified: true }`. Only when that read fails does it fall back to
|
|
201
|
+
this project's record of its last publish, said as unverified (`verified: false`). With
|
|
202
|
+
`--static` the new frontend is published again (the URL may change), and a URL it no longer serves
|
|
203
|
+
at is still named under `staticRemoved`. Re-run with `--static <dir>` to publish again, or merge (`--keep-data`; under
|
|
204
|
+
`--to`, drop `--replace`), which leaves the frontend serving.
|
|
205
|
+
|
|
206
|
+
**Liveness verification** — the build endpoint returning `200` only means the archive was
|
|
207
|
+
*accepted*; the edge may still be starting a cold host (which `503`s for tens of seconds) or
|
|
208
|
+
briefly routing the previous build. So after the upload the CLI polls the deployed URL until
|
|
209
|
+
the static server reports it is serving **this** build — its `X-Xano-Canonical` response header
|
|
210
|
+
matches the canonical returned for the build just pushed — then prints `Frontend is live`. It
|
|
211
|
+
polls every second for the first 30s, then every two seconds out to 120s. An unconfirmed poll
|
|
212
|
+
is a **warning, not a failure** (the build uploaded fine and usually comes online moments
|
|
213
|
+
later; the exit code stays `0` and the summary records `"verified": false`). Verification is
|
|
214
|
+
skipped when the response carries no canonical to compare against. Pass **`--skip-liveness`** to
|
|
215
|
+
skip the wait entirely — useful for fast iterative deploys or when the deployed URL isn't
|
|
216
|
+
reachable from the machine running the CLI.
|
|
217
|
+
|
|
218
|
+
**Config injection** — before archiving, the deploy rewrites EVERY `.html` document in the
|
|
219
|
+
build, inserting an inline `<script>` at the top of `<head>` that assigns each config value to a
|
|
220
|
+
`window.<KEY>` global (so it runs before the app bundle). The backend URL is seeded
|
|
221
|
+
automatically as `window.XANO_HOST` (from the backend deploy's own response), and
|
|
222
|
+
`--static-env KEY=VALUE` (repeatable) merges in extra keys, overriding the seed on a name
|
|
223
|
+
clash. A static host has no server runtime — it serves these files verbatim — so injected
|
|
224
|
+
values are **public**: base URLs and *publishable* keys only, never secrets (those go in
|
|
225
|
+
backend env, read via `env(name)`). Rewriting every document, not just the root, is what a
|
|
226
|
+
prerendered build needs: it serves a different document per route, so a root-only injection
|
|
227
|
+
leaves every deep link and refresh running with the global unset — and the page still renders.
|
|
228
|
+
Injection is skipped (reported as a warning, not a failure) when the archive has no document
|
|
229
|
+
with a `<head>` to anchor to, and any individual document that lacks one is named; values are
|
|
230
|
+
`<`-escaped so one containing `</script>` can't break out of the element. This is why a
|
|
231
|
+
prebuilt `frontend/dist` can retarget any environment with no rebuild.
|
|
232
|
+
|
|
233
|
+
> **Caching — verify with a cache buster.** The static host serves `index.html` with
|
|
234
|
+
> `Cache-Control: public, max-age=3600`, so a browser (or CDN) that loaded the page before
|
|
235
|
+
> your latest deploy can hold the old HTML — including a *pre-injection* `<script>`-less
|
|
236
|
+
> version — for up to an hour. If `window.XANO_HOST` looks missing, it's almost always this:
|
|
237
|
+
> hard-reload (Cmd/Ctrl+Shift+R) or open DevTools with "Disable cache" checked. When
|
|
238
|
+
> verifying from a script or agent, append a throwaway query param so you never read a cached
|
|
239
|
+
> copy — `curl -s "$URL/?nocache=$(date +%s)"` — and check the fetched HTML for the injected
|
|
240
|
+
> `window.XANO_HOST` line rather than retrying the same cached URL.
|
|
241
|
+
|
|
242
|
+
`xanosdk status` prints the same **environment base URL** out of band, for cases where you'd
|
|
243
|
+
rather bake it in at build time — along with the environment's name and expiry, without your
|
|
244
|
+
having to know either. (`xanosdk whoami` prints the *instance* base URL, i.e. the account's
|
|
245
|
+
origin rather than the environment.) A static failure after a committed backend deploy **does not roll
|
|
246
|
+
back**: it exits `3` (`9` when the upload was sent and its answer lost) and prints the `xanosdk publish` command that retries just that
|
|
247
|
+
step, without re-importing the backend.
|
|
248
|
+
|
|
249
|
+
**Publishing a frontend on its own** — `xanosdk publish <dir>` uploads an already-built
|
|
250
|
+
directory and nothing else: no compile, no backend import. `--to` picks the destination —
|
|
251
|
+
`ephemeral[:<name>]`, `workspace`, or `tenant:<name>`; left off, the backend this project last
|
|
252
|
+
deployed to — and a workspace or tenant asks before replacing what its users are served
|
|
253
|
+
(`--yes` in CI). `publish` does not reach a local engine, so after a local-engine deploy it
|
|
254
|
+
refuses the tracked engine and names `--to ephemeral`; the engine takes a frontend through
|
|
255
|
+
`xanosdk deploy --local-engine --static <dir>` instead (below). Scaffolded projects carry it as `npm run xano:deploy:frontend`,
|
|
256
|
+
which rebuilds first.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
xanosdk promote v2 --set-live # the backend, landed and live
|
|
260
|
+
xanosdk publish ./frontend/dist --to workspace --release v2 --branch <label> --yes
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The destination's own URL is injected as `window.XANO_HOST` at publish time (under any
|
|
264
|
+
`--static-env`), so a byte-identical build published to an ephemeral and to your workspace
|
|
265
|
+
serves two different HTML documents. That is the build-once, configure-per-destination
|
|
266
|
+
model, not drift.
|
|
267
|
+
|
|
268
|
+
A release carries no frontend and nothing records which release a static host is serving,
|
|
269
|
+
so the pairing is checked only as far as it can be, and `--json` says how far in `association`:
|
|
270
|
+
|
|
271
|
+
| `association` | What was checked |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `none` | No `--release`. |
|
|
274
|
+
| `release-exists` | `--release` names a release in this workspace — nothing more. |
|
|
275
|
+
| `live-branch` | …and `--branch` is the label your workspace is serving. A label match, not a comparison of the branch's contents with the release. |
|
|
276
|
+
|
|
277
|
+
`--branch` refuses to publish when the label is not live, since the frontend would otherwise
|
|
278
|
+
sit in front of a backend it was not built for. It applies to `--to workspace` only.
|
|
279
|
+
Publication and rollout are reported apart — `published: true` with `verified: false` and
|
|
280
|
+
`verification: "unconfirmed"` means the upload succeeded but the host was not seen serving it
|
|
281
|
+
inside the wait window. Unlike `deploy --static`, `publish` holds no seed rows, so it cannot scan
|
|
282
|
+
the build for non-public seed values. An `index.html` that loads `.ts`/`.tsx`/`.jsx` source (the
|
|
283
|
+
unbuilt source directory, not its build output) is still published, with a warning on stderr and
|
|
284
|
+
in `--json` `warnings` naming the build step.
|
|
285
|
+
|
|
286
|
+
`deploy` reuses cached tokens and **refreshes them automatically** when the access token
|
|
287
|
+
expires (Xano rotates the refresh token on every use; the new one is persisted). A rejected
|
|
288
|
+
refresh (`invalid_grant`) clears the stale cache and tells you to `xanosdk login` again.
|
|
289
|
+
|
|
290
|
+
**CI & agents** run non-interactively. The credential to reach for is the **meta credential
|
|
291
|
+
as three environment variables** — the `type: "token"` record above, with no file:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
XANO_INSTANCE_URL=https://your-instance.xano.io \
|
|
295
|
+
XANO_WORKSPACE_ID=3 \
|
|
296
|
+
XANO_META_TOKEN=your-meta-api-token \
|
|
297
|
+
npx xanosdk deploy ./xano/index.ts
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Nothing is read from or written to disk, and nothing rotates, so the same three secrets keep
|
|
301
|
+
working run after run — which is what makes this the right shape for a CI job. It **outranks
|
|
302
|
+
every other credential**, including an explicit `--config` path, `$XANO_PROFILE` and
|
|
303
|
+
`$XANO_REFRESH_TOKEN`; whichever it displaces is named on stderr, so it never wins silently.
|
|
304
|
+
|
|
305
|
+
An explicit `--profile` is the one exception: it is a **hard error** rather than a
|
|
306
|
+
displacement, because it selects a stored credential that the environment has already
|
|
307
|
+
outranked, and acting on a different tenant with a contradicting flag on the command line
|
|
308
|
+
is the failure this ladder exists to prevent.
|
|
309
|
+
|
|
310
|
+
All three are required **together**. Setting some but not others is a hard error naming the
|
|
311
|
+
rest, rather than a quiet fallback to another credential — a workflow with one misspelled
|
|
312
|
+
secret must not deploy against whatever happens to be on the runner.
|
|
313
|
+
|
|
314
|
+
> **Automated agents:** do **not** invoke `xanosdk login` — it blocks on interactive browser
|
|
315
|
+
> consent. Use the three variables above.
|
|
316
|
+
>
|
|
317
|
+
> The older `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works (both copied once
|
|
318
|
+
> from the profile in `auth.json` after a local `xanosdk login`; the target instance is read from the refresh
|
|
319
|
+
> token's `aud` and the workspace resolved per run), but Xano **rotates refresh tokens on
|
|
320
|
+
> use** — a stored one is spent by its first exchange, so a job that runs twice fails the
|
|
321
|
+
> second time. Prefer the meta credential.
|
|
322
|
+
|
|
323
|
+
## Checking against a live instance (xanosdk preflight)
|
|
324
|
+
|
|
325
|
+
`xanosdk preflight` proves your compiled output against a **real, running Xano
|
|
326
|
+
instance** — not a static snapshot. It compiles your workspace, imports it into a
|
|
327
|
+
**fresh ephemeral environment created for that run**, exports it back, and diffs it
|
|
328
|
+
against what you compiled, so you catch three classes of problem a local build can't:
|
|
329
|
+
|
|
330
|
+
1. **Import accepts** — the engine actually accepts the bundle (malformed-but-shaped output is rejected here).
|
|
331
|
+
2. **Round-trip parity** — the workspace the engine stores, re-exported in the same bundle format, matches your compiled JSON after normalization (full object logic included). Every authored kind is diffed — tables, functions, queries, triggers, tasks, and more — each object matched by identity and reported per kind.
|
|
332
|
+
3. **Runtime** (`--runtime`) — each deployed function actually runs on the engine, with no input, with logs surfaced on failure. A function that throws (a failed precondition, say) fails the gate with the engine's message. A function that declares inputs and is refused only for a missing or invalid one is reported as not run (`needs inputs`, and `needsInputs: true` in `--json`) and does not fail the gate — the run said nothing about its logic, so cover it with a unit test that supplies the inputs.
|
|
333
|
+
|
|
334
|
+
It talks only to public meta API routes — the **same** archive import `xanosdk
|
|
335
|
+
deploy` uses, plus the workspace export. There is one way into an instance, so a
|
|
336
|
+
transport bug is one `preflight` reproduces rather than routes around. (It also
|
|
337
|
+
fetches the engine's own XanoScript rendering of the imported environment, but only
|
|
338
|
+
when an installed toolchain module asks for it; the SDK fetches and hands it over,
|
|
339
|
+
and the comparison belongs to the module.)
|
|
340
|
+
|
|
341
|
+
It is non-destructive: nothing you own is written to. Each run creates its own
|
|
342
|
+
ephemeral environment, imports into that, and deletes it afterwards — including when
|
|
343
|
+
the import is rejected or a transport error is thrown. The environment carries a
|
|
344
|
+
short expiry, so even a killed process leaves nothing permanent behind. A fresh
|
|
345
|
+
environment per run is also what makes the diff trustworthy: the objects read back
|
|
346
|
+
can only have come from this bundle, never from what a previous run left.
|
|
347
|
+
|
|
348
|
+
**Setup** — nothing, if you have run `xanosdk login`. With no `XANO_VALIDATE_*`
|
|
349
|
+
variable set, `preflight` uses the credential the rest of the CLI already acts on
|
|
350
|
+
and creates its ephemeral under the workspace that credential pins.
|
|
351
|
+
|
|
352
|
+
To point a run somewhere else — a different instance, or a local Docker one —
|
|
353
|
+
copy `.env.example` to `.env` (gitignored) and fill in a base URL + token. That
|
|
354
|
+
pair takes over whenever it is set, and switching between a cloud dev instance
|
|
355
|
+
and a local Docker one is just a different `XANO_VALIDATE_INSTANCE`:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
# .env
|
|
359
|
+
XANO_VALIDATE_INSTANCE=https://your-instance.xano.io # or http://localhost:8080 for local Docker
|
|
360
|
+
XANO_VALIDATE_TOKEN=your-meta-bearer-token
|
|
361
|
+
# XANO_VALIDATE_WORKSPACE_ID=… # optional; PARENT workspace the run's env is created under (default 1)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
xanosdk preflight ./xano/index.ts # import + round-trip diff, reports per object (every authored kind)
|
|
366
|
+
xanosdk preflight ./xano/index.ts --runtime # + run each deployed function
|
|
367
|
+
xanosdk preflight ./xano/index.ts --capture # + write fetched JSON to ./validate-out (fixture candidates)
|
|
368
|
+
xanosdk preflight ./xano/index.ts --instance http://localhost:8080 # override the target for one run
|
|
369
|
+
xanosdk preflight --bundle ws.json # check an already-exported bundle
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Two sources, and the environment wins. Set any `XANO_VALIDATE_*` variable (or
|
|
373
|
+
pass `--instance`) and the run targets what it names, reading the token from
|
|
374
|
+
`XANO_VALIDATE_TOKEN` — env-only, never a flag, so it stays out of your shell
|
|
375
|
+
history and the process list. A `.env` in the current directory is autoloaded for
|
|
376
|
+
those three variables and nothing else, and a real environment variable beats the
|
|
377
|
+
file. Set none of them and the run falls back to your stored login, where
|
|
378
|
+
`--profile` selects which credential to act as. A half-set environment is an
|
|
379
|
+
error naming the variable it is missing, never a silent fall back to your own
|
|
380
|
+
workspace. A non-zero exit means a check failed; `--verbose` prints full diffs and
|
|
381
|
+
raw engine detail instead of a projected summary.
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
## Wiring the frontend to the backend
|
|
385
|
+
|
|
386
|
+
**Wiring the frontend to the backend.** The deploy bakes the environment's backend URL into
|
|
387
|
+
every HTML document in your build automatically, as a `window.XANO_HOST` global evaluated
|
|
388
|
+
*before* your app bundle — every document, so a prerendered build's deep links and refreshes
|
|
389
|
+
boot with the same backend the root does. So read it at runtime with a build-time fallback and you never have to
|
|
390
|
+
know the URL ahead of time:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const HOST = (typeof window !== "undefined" && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`window.XANO_HOST` is the **environment** URL your deployed APIs answer at (the same value
|
|
397
|
+
`xanosdk status` prints as the environment's URL); it is *not* `xanosdk whoami`, which prints
|
|
398
|
+
your account's instance origin. Because injection happens at deploy time, a prebuilt
|
|
399
|
+
`frontend/dist` retargets any environment with **no rebuild** — ideal for headless agents.
|
|
400
|
+
Add your own public config (base URLs, *publishable* keys) with `--static-env KEY=VALUE`
|
|
401
|
+
(repeatable), exposed the same way as `window.<KEY>`. A static host serves these files
|
|
402
|
+
verbatim to the browser, so everything injected is **public** — never put secrets here;
|
|
403
|
+
those belong in backend env, read server-side via `env(name)`.
|
|
404
|
+
|
|
405
|
+
**Showing a stored file.** A file column comes back as `{ path, name, type, size, meta,
|
|
406
|
+
access, url }`. Don't use its `url`: on a tenant-scoped environment that field addresses the
|
|
407
|
+
instance host *without* the `/tenant/<name>` segment and 404s — as a broken `<img>`, while
|
|
408
|
+
every assertion about the response still passes. Build the URL from `path` and the host you
|
|
409
|
+
already have:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { fileUrl } from "@xano/sdk";
|
|
413
|
+
|
|
414
|
+
<img src={fileUrl(row.avatar, HOST) ?? ""} /> // null for an absent file
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Verifying the injection:** the served `index.html` writes the global in **bracket
|
|
418
|
+
notation** — `window["XANO_HOST"]="…";` — so grep for the bare token `XANO_HOST`, not the
|
|
419
|
+
exact string `window.XANO_HOST` (the dot form is valid to *read* the global in your app,
|
|
420
|
+
but it's not what the file contains, so an exact-string grep for it wrongly reads as "not
|
|
421
|
+
injected"). It can also be served from cache for up to an hour after a deploy — fetch once
|
|
422
|
+
with a cache-buster (`curl -s "$URL/?nocache=$(date +%s)" | grep XANO_HOST`) rather than
|
|
423
|
+
retrying the bare URL.
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
## Deploy targets, and what a release changes
|
|
427
|
+
|
|
428
|
+
**Two targets**, so the dev loop and the production step stay distinct:
|
|
429
|
+
|
|
430
|
+
| Command | Where it goes |
|
|
431
|
+
|---|---|
|
|
432
|
+
| `xanosdk deploy` | A disposable **ephemeral** environment — create-or-refreshed each run, auto-expiring, with its own URL. This is the default and needs no flag. |
|
|
433
|
+
| `xanosdk promote <release>` | Your **main Xano instance** workspace — the production target. Lands a release on a branch named for it (`--set-live` serves it). Its table changes reach live as it lands — see [what a landing does to tables](#what-a-landing-does-to-tables). |
|
|
434
|
+
| `xanosdk tenant deploy <tenant> <release>` | A **customer tenant** — the same release, landed on someone else's deployment. It **replaces** what the tenant serves: anything the release does not carry is removed. Static hosting is kept. |
|
|
435
|
+
| `xanosdk deploy --to <dest>` | The **escape hatch**: merges a local build straight into `workspace` or `tenant:<name>`, skipping the release record. |
|
|
436
|
+
| `xanosdk deploy --local-engine` | A **local engine** on this machine — no Xano account needed ([below](#deploying-to-a-local-engine)). |
|
|
437
|
+
| `xanosdk test` | Nothing — it only reads. Runs the tests an already-deployed backend carries; `--on` picks which one, `local-engine` and `workspace` included. |
|
|
438
|
+
|
|
439
|
+
Not every instance has ephemeral environments enabled. Where they are off, `deploy` says so
|
|
440
|
+
and names who can turn them on — there is no second destination to fall back to. `xanosdk
|
|
441
|
+
export` and `xanosdk preflight` still work meanwhile.
|
|
442
|
+
|
|
443
|
+
Every `deploy` is a **full replace** of the disposable environment — always fresh — unless
|
|
444
|
+
you pass `--keep-data`. Landing somewhere real is the opposite by design: it changes what
|
|
445
|
+
your code defines and leaves the rest of the workspace — including every row in every
|
|
446
|
+
table — alone.
|
|
447
|
+
|
|
448
|
+
A replace clears everything the ephemeral holds, so before it writes, a refresh reads the
|
|
449
|
+
ephemeral and checks for objects **another source landed there** (another project's
|
|
450
|
+
`deploy --to tenant:<name>`, say) — anything this project neither sends nor landed before. As
|
|
451
|
+
with `--keep-data` below, those are deleted only with `--yes` or a yes on a terminal; without
|
|
452
|
+
one the deploy is refused before anything is written (`SDK_PRUNE_OUT_OF_SCOPE`, exit 2, each
|
|
453
|
+
object in `details.outOfScope`, and the rerun with `--yes` printed).
|
|
454
|
+
|
|
455
|
+
### Keeping your data between deploys
|
|
456
|
+
|
|
457
|
+
`xanosdk deploy --keep-data` redeploys to your ephemeral or local engine by **merging** into
|
|
458
|
+
what an earlier deploy put there, instead of replacing it. Code and schema changes land,
|
|
459
|
+
objects removed from the project are deleted, and the rows already in the tables stay. Seed
|
|
460
|
+
rows are not written again.
|
|
461
|
+
|
|
462
|
+
The merge mirrors this project's source: the environment is the project's own sandbox, so
|
|
463
|
+
anything on it the project no longer declares is deleted. The preview tells that apart from
|
|
464
|
+
objects another source put there (a `deploy --to tenant:<name>` naming it from another
|
|
465
|
+
project, say) by the ephemeral's landing record: an object this project landed there is one
|
|
466
|
+
it "no longer declares"; anything else is "not in this project (landed by another source)".
|
|
467
|
+
Those are deleted only with an answer — `--yes`, or a yes on a terminal (a no writes nothing
|
|
468
|
+
and exits 0, as any declined confirmation does). Without one the
|
|
469
|
+
merge is refused before it writes, as `--prune` refuses the same deletion:
|
|
470
|
+
`SDK_PRUNE_OUT_OF_SCOPE`, exit 2, each object in `details.outOfScope` as `{ type, name, label }`
|
|
471
|
+
(the shape `deploy --to --prune` uses), and the rerun with `--yes` printed. A merge that goes ahead names them in `--json`'s `notLanded`. The scaffolded
|
|
472
|
+
`npm run xano:deploy:local-engine` passes it, so the rows you enter through your app survive
|
|
473
|
+
every code change.
|
|
474
|
+
|
|
475
|
+
It merges only into an environment an earlier deploy **finished** filling. Otherwise it
|
|
476
|
+
replaces and seeds as a plain deploy does, and says why:
|
|
477
|
+
|
|
478
|
+
| Reason (`keepDataSkipped` in `--json`) | When |
|
|
479
|
+
|---|---|
|
|
480
|
+
| `new` | Nothing was recorded for this project yet. |
|
|
481
|
+
| `recreated` | The recorded environment is gone — the ephemeral expired, or the engine restarted. |
|
|
482
|
+
| `never-filled` | The environment exists, but no deploy into it completed (or `.xano/ephemeral.json` lost the marker that one did — the message says which). |
|
|
483
|
+
| `reset` | `--reset` was passed; it always wins. |
|
|
484
|
+
|
|
485
|
+
A deploy that fails does not unmark the environment: a refused one wrote nothing, and one
|
|
486
|
+
whose outcome is unknown (its answer was lost, or the instance failed the merge with a 5xx —
|
|
487
|
+
exit 9) may have landed in whole or in part. Check what the environment holds before rerunning;
|
|
488
|
+
either way the next `--keep-data` still merges.
|
|
489
|
+
|
|
490
|
+
The `--json` summary carries `data: "kept"` or `data: "replaced"` on every deploy.
|
|
491
|
+
|
|
492
|
+
Before it writes, a merge prints what it will cost, without prompting:
|
|
493
|
+
|
|
494
|
+
- every table it drops (removed from the project) with its rows, and every column it drops;
|
|
495
|
+
- every column whose **type** changes — only the definition changes, and the stored values stay:
|
|
496
|
+
a value that reads as the new type does (int → text: `11` reads `"11"`), one that does not
|
|
497
|
+
reads `null` while the column has the new type, and reads again if it is retyped back. A
|
|
498
|
+
vector's size is part of its type (`vector(3)` → `vector(4)`, see the table below);
|
|
499
|
+
- every value removed from an **enum** column — rows holding it are not rewritten, and read
|
|
500
|
+
`null` until the value is restored;
|
|
501
|
+
- every **table reference** pointed at another table — the ids the rows hold are kept, and now
|
|
502
|
+
name rows of the new table;
|
|
503
|
+
- every public URL slug a changed `canonical` moves — every endpoint under the old slug stops
|
|
504
|
+
answering;
|
|
505
|
+
- every column that becomes **nullable** — a merge keeps the table's NOT NULL (an insert without
|
|
506
|
+
it still fails) though the export reads nullable, and only this preview can see it; `--reset`
|
|
507
|
+
(or `--replace` under `--to`) recreates the table, which relaxes it;
|
|
508
|
+
- every column that stops being nullable — rows holding `null` there are not rewritten, and read
|
|
509
|
+
back as the type's empty value (int `0`, text `""`; a date column's key is left out of every row);
|
|
510
|
+
- every index it adds or drops;
|
|
511
|
+
- every new table whose seed rows it does not write;
|
|
512
|
+
- every env var whose project value differs from the live one — a merge keeps the live
|
|
513
|
+
value (`xanosdk env set <NAME> <value>` changes it). Only names are printed, never values;
|
|
514
|
+
- every other object it deletes, named `<kind>:<name>` (`function:old_helper`,
|
|
515
|
+
`query:GET ping (apiGroup shop)`).
|
|
516
|
+
|
|
517
|
+
The `--json` summary carries the same lists, each only when it names something:
|
|
518
|
+
`droppedTables`, `droppedColumns`, `retypedColumns` (each `{ table, column, from, to }`, plus
|
|
519
|
+
`stored` — the type its values are held as, when known — `default` and `nullable` when declared,
|
|
520
|
+
`useXdo` on a JSON-storage table,
|
|
521
|
+
and `omittedInsertsFail` / `setInsertsFail` when inserts fail; see below),
|
|
522
|
+
`notNullKept`, `pairedColumns` (`{ table, dropped, added }`: a drop beside an add, not a rename),
|
|
523
|
+
`narrowedEnums` (`{ table, column, removed }`), `retargetedRefs` (`{ table, column, from, to }`),
|
|
524
|
+
`indexChanges` (`{ table, index, action, unique }`), `notNullTightened` (`{ table, column, type }`),
|
|
525
|
+
`storageChanges` (`{ table, from, to }`: the table's `useXdo` before and after),
|
|
526
|
+
`removed`, `unseededTables` (new tables whose seed rows the merge did not
|
|
527
|
+
write), `renamedTables` (each `{ from, to }`: renamed through `xanosdk lock rename`, rows kept),
|
|
528
|
+
`unchangedEnv` and `canonicalChanges` (each `{ kind, name, from, to }`: a public URL slug a
|
|
529
|
+
changed `canonical` moved — every endpoint under `from` stops answering). A realtime server's
|
|
530
|
+
moved slug, or a channel renamed in place, also leaves its `conversation` transcript and
|
|
531
|
+
`at_least_once` replay under the old name (warning `plan.realtime-history-detached`); moving
|
|
532
|
+
the name back restores them. Columns are named
|
|
533
|
+
`table.column` everywhere.
|
|
534
|
+
|
|
535
|
+
The limits, in one place: rows survive only in tables and columns that keep their identity
|
|
536
|
+
(a table renamed through `xanosdk lock rename` keeps its rows; an unrecorded rename is refused,
|
|
537
|
+
below); changed env values and a new table's seeds need `--reset`;
|
|
538
|
+
a local engine's rows last as long as its process (an engine update restarts it empty, and the
|
|
539
|
+
next run seeds); and a test whose `datasource` is `"live"`
|
|
540
|
+
sees the kept rows, not only the seed fixtures.
|
|
541
|
+
|
|
542
|
+
If the merge is refused, nothing is written — neither the environment nor `xano.lock` — and
|
|
543
|
+
the environment keeps its data. Each refusal is exit 2 and names its remedy:
|
|
544
|
+
|
|
545
|
+
- **A rename the lock was not told about** (`SDK_IDENTITY_CONFLICT`, `details.refused:
|
|
546
|
+
"renamePending"`): the merge would drop a table `xano.lock` still pins, with its rows, while
|
|
547
|
+
the project adds a new one. Each is in `details.renames[]` as `{ table, guid, candidates,
|
|
548
|
+
rename, prune }` — `rename` is the `xanosdk lock rename …` to run if it was renamed (the merge
|
|
549
|
+
then renames it and keeps its rows), `prune` the `xanosdk lock prune …` if it was deleted.
|
|
550
|
+
Run one, then deploy again. When the new table's def pins its own `guid:`, `setGuid: { table,
|
|
551
|
+
guid, pins }` replaces `rename`: set that def's `guid:` to `guid` — a def's guid replaces any
|
|
552
|
+
lock entry, so a `lock rename` would not keep the rows. The lock marks such an entry
|
|
553
|
+
`"guid_source": "code"`. An export that re-pins a table to its def's guid keeps the guid it
|
|
554
|
+
replaced as `"replaced"` (warning `lock.repinned-table`), so the merge refuses to drop a
|
|
555
|
+
table still holding it; `prune` is then `lock prune --identity-only table:<name>`.
|
|
556
|
+
The same refusal names a realtime channel (realtime server, api group) renamed in code whose
|
|
557
|
+
children — its messages — now collide by name: its entry in `details.renames[]` is
|
|
558
|
+
`{ kind, name, guid, candidates, rename, conflicts }`, and its `lock rename` moves the
|
|
559
|
+
children's entries with it.
|
|
560
|
+
- **A name the environment holds under another identity** (`SDK_IDENTITY_CONFLICT`,
|
|
561
|
+
`details.refused: "identityConflict"`): the same kind and name, a different guid, so the
|
|
562
|
+
merge would delete the live one and create the project's beside it. Each is in
|
|
563
|
+
`details.conflicts[]` as `{ kind, name, liveGuid, projectGuid }`. Pass `--reset` to rebuild,
|
|
564
|
+
or rename one of the two.
|
|
565
|
+
- **A pinned public URL the merge cannot serve** (`SDK_IDENTITY_CONFLICT`, `details.refused:
|
|
566
|
+
"identityConflict"`, each in `details.conflicts[]` as `{ kind, name, canonical, liveGuid }`):
|
|
567
|
+
a `canonical` written in code is served as declared or the merge is refused. The usual cause
|
|
568
|
+
is an api group (toolset, realtime server) renamed while keeping its `canonical`: a merge
|
|
569
|
+
creates the renamed object beside the old one, which still holds the slug. The entry carries
|
|
570
|
+
`heldBy` and `rename` — the `xanosdk lock rename …` that makes the merge rename it in place,
|
|
571
|
+
keeping its URL. Otherwise pick another `canonical`, or `--reset`.
|
|
572
|
+
- **A unique index the rows already there break** (`SDK_IMPORT_REFUSED`, `details.refused:
|
|
573
|
+
"uniqueViolation"`, the indexes the merge adds in `details.indexes[]` as `{ table, index }`,
|
|
574
|
+
exit 2, nothing written — `deploy --to` too): remove the duplicate rows or the index, then deploy again.
|
|
575
|
+
Under `--to … --seed`, `details.seededIndexes[]` names the unique indexes on seeded tables: a
|
|
576
|
+
seed row colliding with a live row there is fixed by changing the seed value or the live row.
|
|
577
|
+
- **Another source's objects** (`SDK_PRUNE_OUT_OF_SCOPE`, `details.refused:
|
|
578
|
+
"pruneOutOfScope"`, above): rerun with `--yes`, or declare them in this project.
|
|
579
|
+
- **A table trigger in an ephemeral or a tenant** (`SDK_IMPORT_REFUSED`, `details.refused:
|
|
580
|
+
"tableTrigger"`, each in `details.triggers[]` as `{ name, table }`): a merge into one fails
|
|
581
|
+
while the environment holds a table trigger — the instance answers 500, after part of the merge
|
|
582
|
+
may have landed — and removing the trigger from the project does not help while it is live.
|
|
583
|
+
A replace works: rerun with `--reset` (`--replace` under `deploy --to`), which replaces the
|
|
584
|
+
rows with the seed rows. A merge into a workspace is not affected.
|
|
585
|
+
- **A table holding rows switched to the other storage mode** (`SDK_IMPORT_REFUSED`,
|
|
586
|
+
`details.refused: "storageModeChange"`, each in `details.tables[]` as `{ table, from, to, rows }`):
|
|
587
|
+
`useXdo` (the table's own, or the workspace `use_xdo` it inherits) changed. A merge from JSON to
|
|
588
|
+
columns keeps the rows and erases every value for good; columns to JSON fails partway on NOT NULL.
|
|
589
|
+
`deploy --to` and `promote` refuse it too. Keep the mode, or move the rows to a new table
|
|
590
|
+
yourself; an empty table switches, with a note. `tenant deploy` is a replace, and differs:
|
|
591
|
+
JSON to columns converts the table and keeps every value (a note), while columns to JSON is
|
|
592
|
+
refused for every table, empty or seeded — the instance cannot convert a table back to JSON
|
|
593
|
+
storage, and nothing is sent. A `deploy --replace` rebuilds the tables, so it switches either way.
|
|
594
|
+
|
|
595
|
+
A keep-data refusal's `details` carries `refused` (the reason, a string), `conflicts`, `renames`
|
|
596
|
+
and `outOfScope` (each a list, empty when it does not apply), `serverCode` when the environment
|
|
597
|
+
itself refused, and `warnings` when the run had some. A `--keep-data` deploy never falls back to a replace.
|
|
598
|
+
|
|
599
|
+
Another import already running on the instance is not a refusal of the project: any deploy
|
|
600
|
+
(`--keep-data`, a replace, `--to`) exits 8 with `SDK_IMPORT_REFUSED`, `details.refused:
|
|
601
|
+
"importInProgress"`, writes nothing, and prints the same command to run again in a moment.
|
|
602
|
+
`--to` already merges, so `--keep-data --to` is refused; the flags that only mean something
|
|
603
|
+
with `--to` (`--seed`, `--prune`, `--dry-run`, …) stay refused alongside it.
|
|
604
|
+
|
|
605
|
+
### Deploying to a local engine
|
|
606
|
+
|
|
607
|
+
`xanosdk deploy --local-engine` deploys to an engine running on this machine instead of an
|
|
608
|
+
ephemeral — no network round-trip per deploy. It needs nothing configured:
|
|
609
|
+
|
|
610
|
+
- **The first run** resolves the latest published engine for this machine's platform,
|
|
611
|
+
downloads it, checks it against the sha256 its release recorded, and **pins** that version in
|
|
612
|
+
your `package.json` under `"xanosdk": { "@xano/sdk": { "localEngine": "vX.Y.Z" } }` —
|
|
613
|
+
creating a `package.json` holding only that pin when the project has none. Commit that change —
|
|
614
|
+
every checkout of the project then runs the same engine.
|
|
615
|
+
- **Every later run** uses the pinned version, whatever is newest. Each version downloads once
|
|
616
|
+
per machine and is shared by every project on it (`~/.xanosdk/local-engine`, or
|
|
617
|
+
`XANOSDK_LOCAL_ENGINE_HOME`); a cached pin deploys with no network access.
|
|
618
|
+
- **A newer engine** is announced on each deploy (checked at most once an hour; opt out with
|
|
619
|
+
`XANOSDK_NO_UPDATE_CHECK=1`). In an interactive terminal the deploy asks whether to update,
|
|
620
|
+
defaulting to no — a version you declined is only noted afterwards, not asked again. In CI,
|
|
621
|
+
piped output or `--json` it prints a one-line notice and keeps the pin. The pin never moves
|
|
622
|
+
without a yes or an explicit `xanosdk local-engine update`.
|
|
623
|
+
- **A running engine on a different version** than the one the project now resolves to — after
|
|
624
|
+
an update, or after a teammate moved the pin — is stopped and replaced, and the deploy says
|
|
625
|
+
so.
|
|
626
|
+
- **No lifetime limit.** The engine runs until it is stopped. From v0.1.15 an `--ephemeral`
|
|
627
|
+
engine stops itself after an hour by default, so the SDK starts one with `--ttl 0`, and the
|
|
628
|
+
data and frontend it holds survive between deploys. An ephemeral engine you start by hand
|
|
629
|
+
keeps the one-hour default; its `--ttl <duration>` flag changes it.
|
|
630
|
+
|
|
631
|
+
An engine update restarts the engine, and a restarted engine starts **empty**: the next
|
|
632
|
+
`--keep-data` run (which the scaffolded `npm run xano:deploy:local-engine` passes) seeds it
|
|
633
|
+
rather than merging, so rows entered through your app do not survive the upgrade.
|
|
634
|
+
|
|
635
|
+
**Operating on it.** After a local-engine deploy, the engine is the backend this project
|
|
636
|
+
tracks, so bare `xanosdk test run-all`, `env set`, `env pull`, `tables`, `impersonate` and
|
|
637
|
+
`status` reach it — none of them needs a Xano account. Name it explicitly as `local-engine`,
|
|
638
|
+
or `local-engine:<name>` for another one from `xanosdk local-engine list` (the full grammar is in
|
|
639
|
+
[the CLI guide](cli.md#naming-a-backend)). Two commands cannot serve it and refuse it by name:
|
|
640
|
+
`publish` (a local engine takes its frontend from `deploy --local-engine --static`; or pass
|
|
641
|
+
`--to ephemeral`) and `release create`
|
|
642
|
+
(the cut runs on the instance — pass `--from ephemeral`).
|
|
643
|
+
|
|
644
|
+
| Command | What it does |
|
|
645
|
+
|---|---|
|
|
646
|
+
| `xanosdk local-engine update` | Moves the pin to the latest engine (`--version <v>`: that one, downgrades included), downloads it, and restarts the project's running engine on it. Commit `package.json`. |
|
|
647
|
+
| `xanosdk local-engine cache list` | Each cached engine version, its size on disk, and the engines running on it. |
|
|
648
|
+
| `xanosdk local-engine cache clear` | Removes every cached engine (`--version <v>`: just that one). An engine running on a removed version is stopped first; the next deploy fetches it again. |
|
|
649
|
+
|
|
650
|
+
#### A frontend on the local engine, and server rendering
|
|
651
|
+
|
|
652
|
+
`xanosdk deploy <entry> --local-engine --static <dir>` publishes a built frontend to the
|
|
653
|
+
engine's own static host after the import, as on an ephemeral, and reports it as `static.url`:
|
|
654
|
+
`http://<prefix>.localhost:<port>`. Chrome, Firefox and Safari 26 resolve `*.localhost` to this
|
|
655
|
+
machine by themselves (Safari on macOS 15 and earlier does not); a tool that does not resolve it
|
|
656
|
+
can call the engine's own URL with a `Host: <prefix>.localhost:<port>` header. The engine needs a release
|
|
657
|
+
that hosts static sites: an older one answers the upload with a 501, which the deploy reports
|
|
658
|
+
without failing the backend (exit `3`), and `xanosdk local-engine update` moves the pin on.
|
|
659
|
+
|
|
660
|
+
The engine hosts built output only, and refuses a build of more than 20,000 files or 256 MB unpacked.
|
|
661
|
+
The deploy checks both before the import, so an oversize directory stops the run with nothing
|
|
662
|
+
written. A symbolic link in the directory is not uploaded.
|
|
663
|
+
|
|
664
|
+
A SvelteKit app can also render on the server there. Build it with the SDK's adapter (it needs
|
|
665
|
+
`esbuild` in the app: `npm i -D esbuild`). In a project `xanosdk init` scaffolded, SvelteKit's
|
|
666
|
+
config is the object passed to `sveltekit()` in `vite.config.ts` (a `svelte.config.js` is ignored
|
|
667
|
+
there), and it already builds with this adapter: stop prerendering the routes that should render
|
|
668
|
+
per request (the scaffold prerenders every route from `frontend/src/routes/+layout.ts`):
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
// vite.config.ts, inside sveltekit({ ... })
|
|
672
|
+
import adapter from "@xano/sdk/sveltekit";
|
|
673
|
+
// ...
|
|
674
|
+
adapter: adapter({ out: "frontend/dist" }),
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
In a plain SvelteKit project it goes wherever that project configures SvelteKit, writing `build/`:
|
|
678
|
+
`sveltekit({ adapter: adapter() })` in `vite.config.ts` in a project `npx sv create` makes (it
|
|
679
|
+
writes no `svelte.config.js`), or `export default { kit: { adapter: adapter() } }` in a project
|
|
680
|
+
that keeps a `svelte.config.js`.
|
|
681
|
+
|
|
682
|
+
`vite build` then writes the client assets and prerendered pages at the output's root, a
|
|
683
|
+
`404.html` fallback shell, and the server half in `.xano-ssr/`. Deployed with
|
|
684
|
+
`--local-engine --static <that directory>`, the engine serves the files as files and renders every other
|
|
685
|
+
path (a dynamic route, `__data.json`, a form action) through the server half, so a crawler that
|
|
686
|
+
fetches `/products/lamp` gets that product's own `<title>` and meta tags. A server `load` reads
|
|
687
|
+
the engine's own URL as `XANO_HOST` from `$env/dynamic/private`:
|
|
688
|
+
|
|
689
|
+
```js
|
|
690
|
+
// src/routes/products/[slug]/+page.server.js
|
|
691
|
+
import { env } from "$env/dynamic/private";
|
|
692
|
+
export async function load({ params, fetch }) {
|
|
693
|
+
const res = await fetch(`${env.XANO_HOST}/api:shop/products/${params.slug}`);
|
|
694
|
+
return { product: await res.json() };
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
A rendered page gets the same `window.XANO_HOST` (and `--static-env`) globals the deploy writes
|
|
699
|
+
into the prerendered documents.
|
|
700
|
+
|
|
701
|
+
Any other host (an ephemeral, a tenant, your workspace) is not sent `.xano-ssr/`: it serves the
|
|
702
|
+
static half, and answers every other path with the `404.html` shell (status 404), which boots the
|
|
703
|
+
app in the browser. There, a route whose data comes from a server `load` (`+page.server.js`)
|
|
704
|
+
fails: its `__data.json` is not a file on that host. Prerendered routes, and routes with a
|
|
705
|
+
universal `load` or none, work. A prerendered `/404` route (the scaffold has one) stays
|
|
706
|
+
`404.html` only while every page route is prerendered; once one is not, the adapter writes the shell
|
|
707
|
+
there instead (and says so in the build log), so that route boots — an unknown path still answers
|
|
708
|
+
404 and renders `+error.svelte`. Two things to know about the server half:
|
|
709
|
+
|
|
710
|
+
- **It can hold secrets.** SvelteKit inlines `$env/static/private` values into
|
|
711
|
+
`.xano-ssr/server.js`, which is why it is never served: the engine answers `/.xano-ssr/` with a 404.
|
|
712
|
+
- **It runs inside the engine, not in Node.** Module state is per runtime (the engine runs up to
|
|
713
|
+
four per site, so keep state in your API), request and response bodies are buffered (no
|
|
714
|
+
streaming), `request.formData()` reads urlencoded forms only, and there is no
|
|
715
|
+
`AsyncLocalStorage`. A render has 30 seconds and may answer up to 10 MB.
|
|
716
|
+
|
|
717
|
+
#### Where the engine keeps its data
|
|
718
|
+
|
|
719
|
+
The cache above holds the engine **binaries**. A running engine keeps its own **runtime data** —
|
|
720
|
+
its unpacked runtime files, its logs, and the records of running engines — beside them, under
|
|
721
|
+
`engine/` in the same directory (`~/.xanosdk/local-engine`, or `XANOSDK_LOCAL_ENGINE_HOME`): in
|
|
722
|
+
`engine/Library/Caches` on macOS and `engine/.cache` on Linux, the directory holding `dist`,
|
|
723
|
+
`instances` and `logs` folders. Its `logs` folder holds one file per engine start; a start that
|
|
724
|
+
fails names it.
|
|
725
|
+
|
|
726
|
+
Expect it to reach roughly 600 MB after the first deploy, and to grow by a similar amount for
|
|
727
|
+
each engine version you run, since every version unpacks its own copy. `xanosdk local-engine cache
|
|
728
|
+
list` shows its size, and `xanosdk local-engine cache clear` (with no `--version`) removes it with the
|
|
729
|
+
engines once none is running — it names any engine still up; stop it and clear again. The next
|
|
730
|
+
deploy recreates what it needs.
|
|
731
|
+
|
|
732
|
+
Versions before this layout unpacked the same runtime into your own per-user cache directory.
|
|
733
|
+
Nothing runs from that copy now; `cache list` shows it when it is there, and
|
|
734
|
+
`xanosdk local-engine cache clear --legacy-runtime` removes it after asking (`--yes` without a
|
|
735
|
+
terminal).
|
|
736
|
+
|
|
737
|
+
#### Running a different engine without moving the pin
|
|
738
|
+
|
|
739
|
+
To try an experimental build, or a published version the project hasn't moved to, override the
|
|
740
|
+
pin for your own runs instead of editing `package.json`:
|
|
741
|
+
|
|
742
|
+
- `--local-engine=<value>` for one deploy;
|
|
743
|
+
- `XANOSDK_LOCAL_ENGINE_OVERRIDE=<value>` for every deploy in this shell, including
|
|
744
|
+
`npm run xano:deploy:local-engine`, which passes a bare flag.
|
|
745
|
+
|
|
746
|
+
Both take a **version** (`v0.1.8`, or `0.1.8`: that release from the release manager), an
|
|
747
|
+
**`https` URL** to an engine archive, or the **path** to an engine archive on this machine. A
|
|
748
|
+
value on the flag beats the variable, and the variable beats the pin. Plain `http://` is refused
|
|
749
|
+
unless the host is this machine (`127.0.0.1`, `::1`, `localhost`) — the engine is a program the
|
|
750
|
+
deploy runs — and so is any other URL scheme (`ftp://`, `file://`). A path that names no file is
|
|
751
|
+
refused too; all of these are usage errors, reported before anything compiles or downloads.
|
|
752
|
+
|
|
753
|
+
An override never reads or writes the pin and never checks for updates. Each deploy that uses
|
|
754
|
+
the variable says so. A URL is printed only when its download fails, and then as its origin and
|
|
755
|
+
path, never its query string (a pre-signed link carries its credential there). The engine it names
|
|
756
|
+
replaces the project's running engine like any other version change (it starts empty and the
|
|
757
|
+
next `--keep-data` run seeds it); unset the variable and the next deploy goes back to the pinned
|
|
758
|
+
engine the same way.
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
export XANOSDK_LOCAL_ENGINE_OVERRIDE=~/Downloads/engine-experimental.tar.gz
|
|
762
|
+
npm run xano:deploy:local-engine # runs the experimental build; package.json untouched
|
|
763
|
+
unset XANOSDK_LOCAL_ENGINE_OVERRIDE # next deploy is back on the pinned version
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
`XANOSDK_LOCAL_ENGINE_TOKEN`, when set, is sent only to an override URL's host.
|
|
767
|
+
`XANOSDK_ENGINE_RELEASES_URL` points version lookups at a different engine release service. Its
|
|
768
|
+
engines are downloaded fresh on every deploy and kept apart from the version cache, and the
|
|
769
|
+
deploy runs its latest without reading or writing the pin or checking for updates;
|
|
770
|
+
`xanosdk local-engine update` refuses while it is set.
|
|
771
|
+
|
|
772
|
+
### The release flow
|
|
773
|
+
|
|
774
|
+
This is the path to a real destination. Each step leaves something the next one can name:
|
|
775
|
+
|
|
776
|
+
```bash
|
|
777
|
+
xanosdk deploy ./xano/index.ts --test # 1. stand it up on an ephemeral, run its tests
|
|
778
|
+
xanosdk release create v1 # 2. cut a release from what just ran (after a local-engine deploy: --from ephemeral)
|
|
779
|
+
xanosdk promote v1 # 3. land it in your workspace, on a branch named for it
|
|
780
|
+
xanosdk tenant deploy acme v1 # …or on a customer tenant
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Step 2 is what makes the rest re-runnable. A release is a stored record that this code came
|
|
784
|
+
up and answered, so `promote v1` a week later lands the same bytes — and `xanosdk release
|
|
785
|
+
list` says what you have. Cut a release per merge, per tag, or per deploy you want to keep;
|
|
786
|
+
an existing name is refused rather than replaced, because a tenant may be running it.
|
|
787
|
+
|
|
788
|
+
The platform lands a release itself, so `promote` and `tenant deploy` run no local
|
|
789
|
+
pre-flight: no plan preview, no prune scope. They say so before writing. What they have
|
|
790
|
+
instead is the release — a thing that came up, that can be landed again.
|
|
791
|
+
|
|
792
|
+
`promote` does run the identity check `deploy --to workspace` runs, against the release's
|
|
793
|
+
archive and the branch it will land on: a name the workspace gives another object, or a slug
|
|
794
|
+
pinned in code that another workspace serves, refuses with `SDK_IDENTITY_CONFLICT` (exit 2)
|
|
795
|
+
before anything lands — the platform's landing would otherwise add a suffixed second object or
|
|
796
|
+
mint another slug and report success. A release's identities are fixed when it is cut, so the fix
|
|
797
|
+
is a new release. After landing, `promote` reads the branch back: a pinned slug served under
|
|
798
|
+
another value fails the run (`verify: no`, exit 2, `--set-live` not applied); an unpinned one is
|
|
799
|
+
warned about (`promote.canonical-substituted`).
|
|
800
|
+
|
|
801
|
+
##### What a landing does to tables
|
|
802
|
+
|
|
803
|
+
Tables have no branch. Every landing reads the destination's tables first and names every
|
|
804
|
+
effect under one code. `promote`, `deploy --to` and `deploy --keep-data` **merge**: the
|
|
805
|
+
release's definitions go over the rows and the stored values stay. `tenant deploy`
|
|
806
|
+
**replaces**: the tenant ends up with exactly the release's tables, and values are rewritten.
|
|
807
|
+
|
|
808
|
+
| Change | Merge | Tenant deploy | Code |
|
|
809
|
+
|---|---|---|---|
|
|
810
|
+
| table not in the release, or there by name under another guid | left alone | **dropped with every row** (counted, named in the confirmation); the same name under another guid is refused first (`SDK_IDENTITY_CONFLICT`), as `promote` refuses it | `plan.table-drop` |
|
|
811
|
+
| column dropped | values destroyed | values destroyed | `plan.column-drop` |
|
|
812
|
+
| column retyped | values re-read (int → text: `11` reads `"11"`); one that does not fit reads null, and reads again if retyped back | int, decimal and text values **converted for good** (`"1.5"` → `2`; `"hello"` → `0`; to bool: `false`); one that cannot convert stops it partway; bool → decimal converts (`true` → `1`), timestamp → bool converts to `false`; bool → int or text and timestamp → text are re-read; bool → timestamp and timestamp → int or decimal stop it partway while the table holds rows | `plan.column-retype` |
|
|
813
|
+
| vector size changed (`vector(3)` → `vector(4)`) | every stored vector reads null while the column has the new size, and reads again if changed back; an insert that sets a vector fails (SQL `22000`) — add the new size as a new column | stops it partway while the table holds rows | `plan.column-retype` |
|
|
814
|
+
| enum value removed | a row holding it reads null until restored | same | `plan.enum-values-removed` |
|
|
815
|
+
| table reference retargeted | ids kept, now naming rows of the new table | same | `plan.tableref-retarget` |
|
|
816
|
+
| index added or dropped | a unique index over duplicate rows refuses the landing | same | `plan.index-change` |
|
|
817
|
+
| column made nullable | a column created required keeps refusing rows without a value | relaxed | `plan.not-null-kept` (merge) |
|
|
818
|
+
| column made required | a row holding null reads the type's empty value | null **rewritten** to the empty value for good | `plan.not-null-tightened` |
|
|
819
|
+
| new table or column | existing rows read the column's default, else the type's empty value | same | `plan.schema-additive` (promote) |
|
|
820
|
+
|
|
821
|
+
A merge keeps the old type's storage, and an insert that leaves the column unset writes the
|
|
822
|
+
new type's empty value into it — `""` for text, email, password or enum, `{}` for json, `0`
|
|
823
|
+
for a number or timestamp, nothing for a date. Where the old storage cannot hold that, every
|
|
824
|
+
such insert fails with an SQL error:
|
|
825
|
+
|
|
826
|
+
| Retyped to | Fails from | Remedy |
|
|
827
|
+
|---|---|---|
|
|
828
|
+
| text, email, password | int, decimal, timestamp, bool, date, json, uuid, geo | a default the old type reads: `"0"`, `"false"`, `"1970-01-01"`, `"{}"`, a uuid (none for geo) |
|
|
829
|
+
| enum | int, decimal, timestamp, bool, date, json, uuid, geo | an enum value the old type reads |
|
|
830
|
+
| json | int, decimal, timestamp, bool, date, uuid, geo | none — set it on every insert |
|
|
831
|
+
| int, decimal, timestamp, bool | date, uuid, geo | none — set it on every insert |
|
|
832
|
+
| uuid | every type but json; one that SETS it fails too unless held as text, email, password or enum | none — add the uuid as a new column (another name) |
|
|
833
|
+
|
|
834
|
+
A column already declaring such a default is not flagged. A tenant deploy converts all but
|
|
835
|
+
bool and timestamp, so only those two retyped to text fail there (SQL `22P02`). The warning
|
|
836
|
+
names each column with its remedy, and `--json` marks it `omittedInsertsFail: true` (and
|
|
837
|
+
`setInsertsFail: true` when setting it fails too). The old storage is the type the column was
|
|
838
|
+
created with: an earlier merge retype does not change it. An ephemeral's landing record tracks
|
|
839
|
+
it (`stored`); with no record, the note says the prediction assumes the created type. Some
|
|
840
|
+
merge readings differ from the rule in the table above: decimal → int reads truncated (`29.789`
|
|
841
|
+
reads `29`, and whole again once retyped back), decimal → text reads padded to the column's
|
|
842
|
+
scale (`"29.78900"`), bool → text reads `"1"` / `""`, and int → date reads the int as epoch
|
|
843
|
+
milliseconds.
|
|
844
|
+
|
|
845
|
+
A table with `useXdo: true` keeps its fields as JSON, so none of the failures above apply there:
|
|
846
|
+
every value stays as written, an insert that leaves the column unset (or sets a uuid) lands,
|
|
847
|
+
and `--json` marks the column `useXdo: true`. Its readings differ too: decimal → text reads as
|
|
848
|
+
written (`"1.5"`), text or enum → timestamp or date can read as written or as the time of the
|
|
849
|
+
read rather than null, and json → date makes every read of the table fail (HTTP 500) while a
|
|
850
|
+
row holds an object there.
|
|
851
|
+
|
|
852
|
+
`promote` lands on a new branch, but these apply to **live** as it lands, before any
|
|
853
|
+
`--set-live`, and deleting the branch does not undo them. So a promote that alters a table
|
|
854
|
+
is refused (exit 2, `SDK_SHARED_SCHEMA_CHANGE`, every change in `details.changes`, additions
|
|
855
|
+
included) unless you pass `--allow-shared-schema-changes`; additions are named and land, and so
|
|
856
|
+
is a non-unique index added or dropped — it changes how rows are found, not what they hold. A
|
|
857
|
+
unique index added stays refused. A table that drops a column and adds another is called out
|
|
858
|
+
(`details.pairedColumns`): columns have no rename identity, so the old values are destroyed and
|
|
859
|
+
the new column starts empty — to keep them, add the new column in one release, backfill it, and
|
|
860
|
+
drop the old one in the next. A workspace whose
|
|
861
|
+
tables cannot be read is refused the same way. `--json` carries the effects as
|
|
862
|
+
`tableEffects`.
|
|
863
|
+
|
|
864
|
+
A `tenant deploy` of a release without rows keeps the rows of every table the release
|
|
865
|
+
carries, and drops the tables it does not carry. A release cut with `--seed` **replaces**
|
|
866
|
+
the rows of each table it carries rows for — every row there now is deleted — and keeps
|
|
867
|
+
every other carried table's. It names each one with both
|
|
868
|
+
counts before asking (`table items: 300 rows replaced by 3 seed rows`, `seedRowsReplaced`
|
|
869
|
+
in `--json`); off a terminal it needs `--yes`.
|
|
870
|
+
|
|
871
|
+
They differ in what they leave behind. `promote` adds a branch and leaves the live one
|
|
872
|
+
serving. `tenant deploy` replaces what the tenant serves with the release, so an API that
|
|
873
|
+
an earlier landing or a `deploy --to tenant:<name>` merge added is removed. Static hosting
|
|
874
|
+
is kept. Cut a release that carries everything the tenant should serve.
|
|
875
|
+
|
|
876
|
+
A `promote` over rows that break a unique index the release adds lands nothing — no
|
|
877
|
+
branch, live as it was — and exits 2 (`SDK_CONSTRAINT_VIOLATION`, `details.constraint`
|
|
878
|
+
naming the index when the release adds exactly one, `uniqueViolation: true`).
|
|
879
|
+
|
|
880
|
+
A `tenant deploy` that stops partway can leave the tenant partly replaced. When the
|
|
881
|
+
tenant's rows break something the release adds — a unique index over rows that share a
|
|
882
|
+
value, a required field that rows leave empty, a stored value a retype cannot convert
|
|
883
|
+
(`constraint.kind: "conversion"`, naming the column when one retype could cause it) — it
|
|
884
|
+
fails with `completed: "no"`, `partlyReplaced: true`, the `constraint` it hit, and
|
|
885
|
+
`recoverWith`: the `tenant deploy` of the release it ran before, which puts its logic and
|
|
886
|
+
definitions back (values a conversion already rewrote stay rewritten). Fix the rows, then land again. For
|
|
887
|
+
an `unknown` outcome, an unmoved `deployedAt` means the release did not land, not that the
|
|
888
|
+
tenant is untouched: land the previous release again to be sure.
|
|
889
|
+
|
|
890
|
+
#### Reading the result: `--json` and exit codes
|
|
891
|
+
|
|
892
|
+
`release create`, `promote`, `tenant deploy` and `release transfer` answer in one shape. Under `--json`, or
|
|
893
|
+
whenever stdout is not a terminal, stdout holds a single JSON document. The command writes
|
|
894
|
+
it on failure as well as success, once it knows what it was about to write. Every date in a
|
|
895
|
+
`--json` document, from any command, is ISO 8601 UTC (`2026-10-01T09:13:16.000Z`):
|
|
896
|
+
|
|
897
|
+
```jsonc
|
|
898
|
+
{
|
|
899
|
+
"ok": false, // only when the run failed, beside `error`
|
|
900
|
+
"operation": "promote",
|
|
901
|
+
"destination": { "instance": "https://…", "workspaceId": 1, "kind": "workspace" },
|
|
902
|
+
"release": { "instance": "https://…", "workspaceId": 1, "id": 42, "name": "v1" },
|
|
903
|
+
"branch": "v1-20260922T140000Z-3f2a", // the label promote landed on; null for tenant deploy
|
|
904
|
+
"completed": "unknown", // yes | no | unknown
|
|
905
|
+
"steps": [
|
|
906
|
+
{ "name": "land", "completed": "unknown" },
|
|
907
|
+
{ "name": "verify", "completed": "skipped" }
|
|
908
|
+
],
|
|
909
|
+
"resolveWith": "xanosdk workspace branch list --json",
|
|
910
|
+
"resolveSteps": ["If \"v1-…\" is absent, nothing landed: re-run as `xanosdk promote v1 --branch v1-… --yes`.", "…"],
|
|
911
|
+
"startedAt": "2026-09-22T14:00:00.000Z",
|
|
912
|
+
"resolveNotBefore": "2026-09-22T14:10:00.000Z",
|
|
913
|
+
"error": { "code": "SDK_ERROR", "message": "…", "exitCode": 9 } // only when the run failed
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
A run that failed carries `ok: false` and `error` — the same `code` and `message` the failure document
|
|
918
|
+
below carries, and the exit code the run ends with — so `completed: "no"` always arrives
|
|
919
|
+
with its reason. `resolveSteps`, when present, says what to do with each answer `resolveWith` gives. A declined confirmation and a dry run that answered carry none.
|
|
920
|
+
|
|
921
|
+
`destination` is one shape on every command that writes: `{ instance, workspaceId, kind }`
|
|
922
|
+
for the workspace, and for an ephemeral or tenant (`tenant deploy`, `deploy --to tenant:<name>`)
|
|
923
|
+
`{ instance, workspaceId, kind, label, url }` — the instance and workspace it lives under,
|
|
924
|
+
`kind` its actual type (`"ephemeral"` or `"tenant"`, whichever way it was named), `label` its
|
|
925
|
+
bare name, and `url` its own base URL. `branch` is `null` wherever no branch applies — a tenant
|
|
926
|
+
deploy, and a release cut from an ephemeral. Beside it, `selector` spells that destination as `--to` takes
|
|
927
|
+
it (`"workspace"`, `"tenant:acme"`) and `workspaceId` repeats its workspace.
|
|
928
|
+
|
|
929
|
+
`release create` also carries `drift`: whether the source was compared against this
|
|
930
|
+
project's compile (`checked`), how many objects differ, are missing there, or exist only
|
|
931
|
+
there (`count`), and each of them named as `workspace diff` names it (`objects`, a source-only
|
|
932
|
+
one suffixed `(not in this project)`, one missing there `(not on the source)`) — or `checked: false` with the `reason` it could not run.
|
|
933
|
+
|
|
934
|
+
`completed` answers one question: does the outcome you asked for exist?
|
|
935
|
+
|
|
936
|
+
| `completed` | Exit | Meaning | What to do |
|
|
937
|
+
|---|---|---|---|
|
|
938
|
+
| `yes` | 0 | It exists. | Nothing. |
|
|
939
|
+
| `no` | non-zero; 0 when you declined the confirmation | It does not. If the run left something behind (a landed branch, a cut it could not delete), `residue` names it and the command that removes it. | Fix the cause and re-run. |
|
|
940
|
+
| `unknown` | **9** | The write was sent and the answer was lost: a timeout, a dropped connection, a 5xx, or Ctrl-C mid-write. | Run `resolveWith` before anything else. |
|
|
941
|
+
|
|
942
|
+
A run ended by a signal writes `interrupted: true`. With a write on the wire the outcome is `unknown`;
|
|
943
|
+
with none it is `cancelled` (exit 130 for Ctrl-C, 143 for SIGTERM, 129 for SIGHUP). Either way `landed`
|
|
944
|
+
says what this run had already landed, such as the deploy before its rename or static publish.
|
|
945
|
+
|
|
946
|
+
Never retry an `unknown` blind. The platform gives a taken release name a suffix rather
|
|
947
|
+
than refusing it, so a second `release create` after a lost response can leave two
|
|
948
|
+
releases. A read that says "absent" before `resolveNotBefore` is not proof either, because
|
|
949
|
+
the server may still be finishing the write. A step an earlier failure kept from running is
|
|
950
|
+
`skipped`. For `promote`, `verify` only checks the landing: a verification that could not
|
|
951
|
+
run leaves the landing `yes`.
|
|
952
|
+
|
|
953
|
+
A declined confirmation (typing `n`, or Ctrl-D) exits 0 and still answers `--json`. The
|
|
954
|
+
operation commands write their document with the step `no`; `tenant delete`, `ephemeral delete`,
|
|
955
|
+
`release delete` and `workspace branch delete` write `{ verb, destination, name, deleted: false,
|
|
956
|
+
alreadyGone: false, declined: true }` (a branch delete names `branch` instead of `name`).
|
|
957
|
+
`publish` writes its own document with `published: false, declined: true`, and `env set|unset`
|
|
958
|
+
write `{ verb, name, action: null, destination, declined: true }`; each carries `declined: false`
|
|
959
|
+
when it went ahead, so the shape is the same either way.
|
|
960
|
+
|
|
961
|
+
Every other command that fails under `--json` (`deploy` included) writes a failure document
|
|
962
|
+
instead, so stdout is never empty. Branch on `code`, not on the message:
|
|
963
|
+
|
|
964
|
+
```jsonc
|
|
965
|
+
{
|
|
966
|
+
"ok": false,
|
|
967
|
+
"error": {
|
|
968
|
+
"code": "SDK_SEED_IN_STATIC",
|
|
969
|
+
"message": "Refusing to publish: …",
|
|
970
|
+
"exitCode": 1,
|
|
971
|
+
"details": { "leaks": [{ "file": "assets/index-….js", "table": "user", "column": "password" }] }
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
`details` is an object, never a bare list: each code's list sits under its own key, beside
|
|
977
|
+
`warnings` (`{ code, message }` each) when the run printed some.
|
|
978
|
+
|
|
979
|
+
| `code` | Means | Extra fields |
|
|
980
|
+
|---|---|---|
|
|
981
|
+
| `SDK_SEED_IN_STATIC` | `deploy --static` found seed values in the build (below). | `details.leaks`: `{ file, table, column }` per leak |
|
|
982
|
+
| `SDK_EXPORT_INVALID` | The workspace failed an export check, or `--strict` promoted a warning. | `details.diagnostics`: `{ severity, code, message }` per failed check; under `--strict`, every warning that fails it; `code` names the check, e.g. `seed.public-seed`; `details.warnings` beside it, empty when the run printed none |
|
|
983
|
+
| `SDK_CREDENTIAL_REJECTED` | The instance, or its sign-in server, refused the credential this run used: exit 1. The message names the sign-in that fixes it. | `details`: `{ profile, credentialType, instance, workspaceId, signIn }` (`profile` is `null` for an environment credential; `instance` and `workspaceId` are `null` for a refused `XANO_REFRESH_TOKEN`; `signIn` is `null` when the fix is a variable or an upgrade rather than a command) |
|
|
984
|
+
| `SDK_USAGE` | The command line was mistyped or incomplete — an unknown flag, a value on a flag that takes none, a flag without the one it needs (`--dry-run` without `--to`), a missing argument, a confirmation with no terminal to ask on, a local file or directory it names that does not exist: exit 1. | `suggestion`: the closest match, when one is close enough |
|
|
985
|
+
| `SDK_ERROR` | Any failure without a code of its own yet — including a NAMED thing that cannot be found (a backend, release, branch, static host, test, profile or local-engine version: exit 8). A network failure or a server error (5xx) is exit 8 before anything is written — the backend's lookup, a read-only command's read (`tables`, `test list`, `env pull`, `status`, `whoami`, `workspace details`, `workspace branch`'s list), a local engine's release lookup or download, and `--keep-data`'s reads ahead of its merge (whether the environment can merge, its live read, the merge preview) got no answer or a 5xx; rerun as printed — and exit 1 at a later step that never reached it (an import that was not sent: nothing was written — retry). A `501` on `--keep-data`'s check is an answer, not an outage: that backend cannot merge, so it is exit 1 with the deploy that replaces instead. | `suggestion`: the near name a not-found one is one slip from, when there is one (`suggestions`: every one, when a command missed several) |
|
|
986
|
+
|
|
987
|
+
A piped stdout gets the document too, without `--json`, so a script reading the pipe never gets
|
|
988
|
+
an empty stdout. The exception is a command whose stdout is the data itself (`export` to stdout,
|
|
989
|
+
`llms`, `completion`, `--help`): a failure appended to it would corrupt it, so those write one
|
|
990
|
+
only under `--json`. A piped `release` or `promote` write prints its operation result (above)
|
|
991
|
+
either way.
|
|
992
|
+
|
|
993
|
+
`SDK_SEED_IN_STATIC` means `deploy --static` found seed values from a non-public column in the
|
|
994
|
+
build. If they are deliberately public (a demo login the frontend shows), declare the columns on
|
|
995
|
+
the table with `publicSeed: ["password"]`. Every deploy path then reads the same declaration.
|
|
996
|
+
|
|
997
|
+
#### Guarding what is live
|
|
998
|
+
|
|
999
|
+
`promote --set-live --expect-live v1` lands only if `v1` is the live branch when the
|
|
1000
|
+
promote starts. It checks again immediately before switching live, and if live moved in
|
|
1001
|
+
between, the new branch stays landed but is not served. The platform has no
|
|
1002
|
+
compare-and-set, so the check narrows the race; it does not close it.
|
|
1003
|
+
`workspace branch set-live <branch> --expect-live v1` takes the same guard, checked before
|
|
1004
|
+
it asks and again before it switches. `deploy --set-live` does not take it.
|
|
1005
|
+
|
|
1006
|
+
A check that does not hold is a conflict: exit 2, `SDK_LIVE_BRANCH_MISMATCH`, and the branch
|
|
1007
|
+
that is live under `details.conflictsWith.live` (`null` when no labelled branch is).
|
|
1008
|
+
|
|
1009
|
+
#### What a release carries
|
|
1010
|
+
|
|
1011
|
+
Every table's **schema**, always. That part is not selectable, so a release cut with no
|
|
1012
|
+
extra flags is not a release "without tables" — it has all of them.
|
|
1013
|
+
|
|
1014
|
+
What it does not carry, unless you ask for it, is any table's **rows**:
|
|
1015
|
+
|
|
1016
|
+
```bash
|
|
1017
|
+
xanosdk tables # id, guid, and name — of what this project deployed to
|
|
1018
|
+
xanosdk tables ephemeral:e4f2-9ab1 # …or a named env (its name, not its display name)
|
|
1019
|
+
xanosdk tables tenant:acme # …or a tenant
|
|
1020
|
+
xanosdk tables workspace # …or your workspace
|
|
1021
|
+
xanosdk release create v1 --seed # every table's rows ride along
|
|
1022
|
+
xanosdk release create v1 --seed=<guid>,<guid> # only those two tables' rows
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
Rows are opt-in because a release is durable and can be landed on another environment; data
|
|
1026
|
+
should not travel there by accident. When a selected table has a column the schema marks
|
|
1027
|
+
non-public, the cut names it and asks before going ahead (`--yes` answers in advance).
|
|
1028
|
+
|
|
1029
|
+
Seeded **password** columns carry hashes, and a password hash is keyed to the environment that
|
|
1030
|
+
made it — every workspace, ephemeral and tenant has its own key, and no release carries it. The
|
|
1031
|
+
hashes land unchanged, so logins against those rows work only back on the environment the
|
|
1032
|
+
release was cut from; anywhere else each one answers as a bad password. A landing that hits this
|
|
1033
|
+
warns and names the columns (`passwordHashesUnverifiable` in `deploy --json` and `tenant deploy --json`). To log in
|
|
1034
|
+
elsewhere, deploy the entry file (its seed sends plaintext, which the destination hashes) or
|
|
1035
|
+
reset those passwords after landing. A standard tenant takes only a release, so there the
|
|
1036
|
+
choices are a reset on the tenant, or a release cut without that table's rows.
|
|
1037
|
+
|
|
1038
|
+
A release carries no file **bytes**. A file column's value and an MCP icon declared with
|
|
1039
|
+
`hostedFile()` both point into the source's file library, and a release cut from it copies the
|
|
1040
|
+
pointer without the file — on the backend it lands on, the row's file would be empty and the
|
|
1041
|
+
icon would answer 404. So `release create` refuses (`SDK_RELEASE_FILES_NOT_CARRIED`, exit 2) a
|
|
1042
|
+
cut whose source serves `hostedFile()` icons, or whose `--seed` selects a table with a file
|
|
1043
|
+
column while the source holds files. Land a project like that by deploying it directly, which
|
|
1044
|
+
ships its files: `xanosdk deploy --to tenant:<name> --seed`.
|
|
1045
|
+
|
|
1046
|
+
List from **the source you are cutting from**, not from your workspace. A table this project
|
|
1047
|
+
deployed carries the guid `xano.lock` pins for it wherever it landed, but one created anywhere
|
|
1048
|
+
else — in the dashboard, by another project — has a guid its own backend assigned, so a guid read
|
|
1049
|
+
off one backend is not proof of the same table on another. That is what `tables <backend>` is for.
|
|
1050
|
+
|
|
1051
|
+
#### Why guids, and the one thing to know about them
|
|
1052
|
+
|
|
1053
|
+
Selection is by **guid**. Not by id, and not by name, and both exclusions are about how the
|
|
1054
|
+
wrong choice *fails* rather than about convenience.
|
|
1055
|
+
|
|
1056
|
+
An **id** is a small dense integer, so an id from the wrong backend almost always exists on
|
|
1057
|
+
this one — and names a different table. The server does not refuse an identifier it does not
|
|
1058
|
+
recognize; it ignores it and answers 200. So a stale id cuts a release that reports success
|
|
1059
|
+
and carries the wrong rows into something you can promote to production. A stale **guid** is
|
|
1060
|
+
simply absent from the source's listing, so the cut is refused before anything is written.
|
|
1061
|
+
|
|
1062
|
+
A **name** is the one identifier you can change, so a saved name selection silently
|
|
1063
|
+
re-targets at whatever holds that name after a rename — the same failure by a different
|
|
1064
|
+
route. Names are also unfiltered free text (they may contain a comma), so a comma-separated
|
|
1065
|
+
list of them could not be read back reliably.
|
|
1066
|
+
|
|
1067
|
+
> **A guid survives a redeploy.** A deploy sends the guids `xano.lock` pins — derived from each
|
|
1068
|
+
> table's name (`md5(dbo:<name>)`, 32 hex characters) — and the import keeps them, a full replace
|
|
1069
|
+
> included: the table's **id** moves on every replace, its guid does not. So a guid you wrote down
|
|
1070
|
+
> keeps matching after the next deploy, and is preserved **exactly** from there on: cut a release
|
|
1071
|
+
> from that environment and promote it into a workspace branch and the guid is byte-identical at
|
|
1072
|
+
> every hop, while the ids move.
|
|
1073
|
+
>
|
|
1074
|
+
> What changes one is a **rename** — the name is what the guid derives from. A 32-hex guid that
|
|
1075
|
+
> no longer matches usually means the table was renamed or removed in code since it was noted;
|
|
1076
|
+
> `xanosdk lock rename` carries the identity across a rename you meant. A guid in the engine's own
|
|
1077
|
+
> shape (about 27 base64url characters) belongs to a table this project did not deploy, and does
|
|
1078
|
+
> not travel between backends.
|
|
1079
|
+
|
|
1080
|
+
`release create` checks every guid against the source it is about to read, prints that
|
|
1081
|
+
source's real tables if one does not match, and names the listing verb for that source.
|
|
1082
|
+
|
|
1083
|
+
### The alternative: merging a local build
|
|
1084
|
+
|
|
1085
|
+
`xanosdk deploy <entry> --to workspace` (or `--to tenant:<name>`) skips the release record and
|
|
1086
|
+
merges the build on your disk straight into the destination. Reach for it when you want the
|
|
1087
|
+
pre-flight the release path does not have — a plan preview, a prune scope, loss warnings —
|
|
1088
|
+
or when you are deleting objects, which a release does not do.
|
|
1089
|
+
|
|
1090
|
+
What it leaves behind is the **landing record** in `xano.lock`: which identities this project
|
|
1091
|
+
wrote to that destination, and the whole scope of a later `--prune` there (see below). Commit
|
|
1092
|
+
the lock after it. What it does not leave is a release: no stored archive, nothing to land again
|
|
1093
|
+
or on a second destination, nothing for a tenant to be pointed at, and no record of what the
|
|
1094
|
+
build *was* beyond the commit you built it from. Prefer the release flow for anything you may
|
|
1095
|
+
need to repeat.
|
|
1096
|
+
|
|
1097
|
+
The flags that describe the ephemeral a plain deploy refreshes — `--name`, `--expires-hours`,
|
|
1098
|
+
`--reset`, `--keep-data`, `--test` (with `--kind`/`--concurrency`), `--no-dev-env`,
|
|
1099
|
+
`--require-microservices`, `--open` — are refused beside `--to`, before sign-in. `--static`
|
|
1100
|
+
publishes beside the merge, but not under `--dry-run`; `--skip-liveness` needs `--static` there.
|
|
1101
|
+
|
|
1102
|
+
| `deploy --to` flag | What it does to the destination |
|
|
1103
|
+
|---|---|
|
|
1104
|
+
| *(none)* | Adds new objects, updates existing ones. Nothing is deleted, no rows are written. |
|
|
1105
|
+
| `--dry-run` | Prints the plan and exits without sending anything. |
|
|
1106
|
+
| `--prune` | Also deletes objects **this project landed on this destination** and no longer defines — per the `landed` record in `xano.lock`, see below. |
|
|
1107
|
+
| `--reset-data` | Empties every table the bundle carries. |
|
|
1108
|
+
| `--seed` | Writes the bundle's table rows **by id**: a row the destination holds at a seed row's id is overwritten (a seed row with no `id` is numbered by position, 1, 2, …). The plan names each table's rows written and how many overwrite a live row, and the confirmation repeats the count. Combine with `--reset-data` to reset **and** re-seed. |
|
|
1109
|
+
| `--replace` | The disposable-environment behavior: wipe the workspace — **including every branch** — and import in its place. See below. |
|
|
1110
|
+
| `--branch <label>` | Land on a branch instead of the live one. See below. |
|
|
1111
|
+
| `--set-live` | Promote that branch to live once the import succeeds. |
|
|
1112
|
+
| `--backup-branch[=<label>]` | Snapshot the live branch's logic first. Switching back restores logic only — see below. |
|
|
1113
|
+
| `--allow-shared-schema-changes` | Proceed even though table and microservice changes reach every branch. |
|
|
1114
|
+
| `--allow-branch-deletion` | Proceed even though `--replace` permanently deletes the workspace's other branches. |
|
|
1115
|
+
|
|
1116
|
+
#### `--replace` clears by workspace, not by branch
|
|
1117
|
+
|
|
1118
|
+
The name suggests it replaces what you are deploying. It does not: the clear runs
|
|
1119
|
+
against the whole **workspace**, so every branch in it is deleted — not hidden,
|
|
1120
|
+
not detached — along with the vault secrets, services, marketplace installs,
|
|
1121
|
+
request history and the **saved versions** that would otherwise be the way back.
|
|
1122
|
+
Nothing restores them afterwards.
|
|
1123
|
+
|
|
1124
|
+
The live branch comes back, because the archive recreates it. Every other branch
|
|
1125
|
+
does not, and that includes the `ide-before-*` and `v2-*` snapshots the platform
|
|
1126
|
+
takes on your behalf — a real workspace was measured holding 1,841 of them.
|
|
1127
|
+
|
|
1128
|
+
So a replace is refused when the workspace has any non-live branch:
|
|
1129
|
+
|
|
1130
|
+
```
|
|
1131
|
+
✗ Refusing to `--replace` workspace #33: it has 6 non-live branches — 1 yours,
|
|
1132
|
+
5 platform backups, and the clear deletes branches by WORKSPACE.
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
The plan lists them before anything is written (the first ten, plus a count), and
|
|
1136
|
+
marks which were made by the platform, so the number that decides whether to go
|
|
1137
|
+
ahead — how many are **yours** — is the first thing you read. Branches the engine
|
|
1138
|
+
reports with no label are counted too: they cannot be named or exported, and a
|
|
1139
|
+
replace deletes them like any other.
|
|
1140
|
+
|
|
1141
|
+
`--yes` does not cover this; it waives the confirmation, not the loss. To go
|
|
1142
|
+
ahead, pass `--allow-branch-deletion`. To keep the work first, export each branch
|
|
1143
|
+
on its own — a bare `xanosdk workspace export` reads the **live** branch, which is
|
|
1144
|
+
the one a replace restores anyway:
|
|
1145
|
+
|
|
1146
|
+
```sh
|
|
1147
|
+
xanosdk workspace export --branch branch2
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
Or drop `--replace`: the default merge leaves other branches untouched. Tenants
|
|
1151
|
+
have no branches, so none of this applies to `--to tenant:<name>`.
|
|
1152
|
+
|
|
1153
|
+
Under `--json`, the same facts arrive as `branchDeletion` — an exact `total`, the
|
|
1154
|
+
`authored` / `platformBackups` split, an `unlabeled` count for the rows that
|
|
1155
|
+
cannot be named, and the `labels` that can — so a tool can show the scope before
|
|
1156
|
+
it asks anyone to approve it. `unknown: true` means the inventory could not be
|
|
1157
|
+
read, which is not the same as nothing to lose.
|
|
1158
|
+
|
|
1159
|
+
### Where a release lives
|
|
1160
|
+
|
|
1161
|
+
A release is always a record **in your workspace**, whatever it was cut from —
|
|
1162
|
+
that is what `xanosdk release list`, `promote`, and `tenant deploy` all read.
|
|
1163
|
+
|
|
1164
|
+
Cutting from an environment is one call. The platform reads that environment's
|
|
1165
|
+
live state and writes the record here — **nothing is written to the
|
|
1166
|
+
environment**, so a disposable one expiring later takes nothing with it and a
|
|
1167
|
+
sandbox you cut from is left exactly as it was.
|
|
1168
|
+
|
|
1169
|
+
Only a throwaway environment may be a source. `--from tenant:<name>` is accepted
|
|
1170
|
+
for an ephemeral or a sandbox, and refused for a `standard` or `run` tenant: that
|
|
1171
|
+
is a live deployment. `xanosdk tenant get <name>` names the last release landed
|
|
1172
|
+
there, which lands anywhere as it is — but a `deploy --to` merge since may have
|
|
1173
|
+
changed what the tenant runs. To start from what it runs now, `xanosdk pull` it,
|
|
1174
|
+
deploy that to an ephemeral, and cut from there.
|
|
1175
|
+
|
|
1176
|
+
Cutting `--from workspace` packages a branch directly — whatever the workspace
|
|
1177
|
+
currently serves, or the one `--branch <label>` names.
|
|
1178
|
+
|
|
1179
|
+
An instance that predates the one-call cut does not reject the request; it
|
|
1180
|
+
ignores the source and cuts from your workspace instead. The CLI confirms every
|
|
1181
|
+
sourced cut against the workspace audit log, and if the source was not honored it
|
|
1182
|
+
deletes the release it just created and refuses, rather than leaving one whose
|
|
1183
|
+
contents are not what its name claims.
|
|
1184
|
+
|
|
1185
|
+
### Moving a release to another workspace or instance
|
|
1186
|
+
|
|
1187
|
+
`xanosdk release transfer <name> --to-profile <profile>` copies a stored release into the
|
|
1188
|
+
workspace a stored credential profile is bound to — another workspace, or another
|
|
1189
|
+
instance. It moves the archive and nothing else: no compile, no branch, nothing lands.
|
|
1190
|
+
Run `promote` on the destination afterwards, exactly as you would at the source.
|
|
1191
|
+
|
|
1192
|
+
```bash
|
|
1193
|
+
xanosdk release transfer v1 --to-profile prod --dry-run # is it already there?
|
|
1194
|
+
xanosdk release transfer v1 --to-profile prod --yes # copy it
|
|
1195
|
+
xanosdk promote v1 --profile prod # land it there
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
`--to-profile` names a profile from `xanosdk profile list`; the run's own credential is the
|
|
1199
|
+
source. A profile bound to the same instance and workspace is refused.
|
|
1200
|
+
|
|
1201
|
+
A transfer decides "already there" by content, never by name or size: the sha256 of the
|
|
1202
|
+
archive's tar, which is the same on every host that stores it. So it is safe to re-run.
|
|
1203
|
+
|
|
1204
|
+
| The destination holds… | Result |
|
|
1205
|
+
|---|---|
|
|
1206
|
+
| a release with the same content | Reused — nothing imported, `completed: yes`, `reused: true`. |
|
|
1207
|
+
| a **different** release under the name | Refused, exit `2`. Releases are never replaced; `conflictsWith` names the holder. |
|
|
1208
|
+
| neither | Imported under the archive's own name, read back, and hashed again. |
|
|
1209
|
+
|
|
1210
|
+
`--dry-run` writes nothing and answers `present`: `identical`, `conflict` or `absent`.
|
|
1211
|
+
Under `--json` the result is the shape above, with steps `read`, `search`, `import`, `verify`
|
|
1212
|
+
(`read` is the source side, so a release that is not there fails as `read: no`, exit `8`);
|
|
1213
|
+
`release` is the destination's copy, `source` is where it came from, and `sha256` is the
|
|
1214
|
+
digest both sides agree on. A transfer never deletes: if what landed does not hash to what
|
|
1215
|
+
was sent, it exits `2` and `residue` names the import and the `release delete` that
|
|
1216
|
+
removes it.
|
|
1217
|
+
|
|
1218
|
+
### Pulling a backend into a project
|
|
1219
|
+
|
|
1220
|
+
`xanosdk pull <source>` refreshes `xano/` in a project that already has a frontend, a lockfile,
|
|
1221
|
+
and a git history — where `xanosdk init --from <source>` writes a whole new project instead. It
|
|
1222
|
+
takes the same source vocabulary everything else does: `release:<name>`, `ephemeral:<name>`,
|
|
1223
|
+
`tenant:<name>`, `workspace`, or a bundle path.
|
|
1224
|
+
|
|
1225
|
+
```bash
|
|
1226
|
+
xanosdk pull release:v1 # refresh xano/ from a release (keeps files you added)
|
|
1227
|
+
xanosdk pull workspace # …or from the live workspace
|
|
1228
|
+
xanosdk pull release:v1 --yes # skip the confirmation and the dirty-tree refusal (CI)
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
`pull` **replaces** and cannot merge: a file the previous decode wrote that this one does not
|
|
1232
|
+
is deleted, and a decoded file is rewritten, hand edits included. A file no decode wrote (a def
|
|
1233
|
+
you added, notes) is kept and named. It lists the deletions, asks, and refuses a dirty working
|
|
1234
|
+
tree.
|
|
1235
|
+
|
|
1236
|
+
`xano.lock` is **reconciled**, not preserved — an entry survives only when the source carries an
|
|
1237
|
+
object with the same key *and* the same identity, so a lock cannot pin a guid the new backend
|
|
1238
|
+
never had. If none of the lock's identities appear in the source, that is what pointing a
|
|
1239
|
+
project at an unrelated backend looks like, and the pull is refused unless you pass `--yes`.
|
|
1240
|
+
|
|
1241
|
+
### Deploying to a branch
|
|
1242
|
+
|
|
1243
|
+
`--branch <label>` stages on a Xano branch instead of the live one, which is the safer shape
|
|
1244
|
+
when a workspace has no staging twin. `deploy --to workspace --branch <label>` creates the
|
|
1245
|
+
branch as a clone of live, and refuses a label that already exists (exit 2,
|
|
1246
|
+
`SDK_BRANCH_TAKEN`) — pick a new label for each deploy, or delete the old branch first. Nothing
|
|
1247
|
+
serves the branch until you promote it.
|
|
1248
|
+
|
|
1249
|
+
`promote` lands a release on a branch and takes the same flag:
|
|
1250
|
+
|
|
1251
|
+
```bash
|
|
1252
|
+
xanosdk promote v1 --branch staging # land the release there, live untouched
|
|
1253
|
+
xanosdk workspace branch list # see what exists, and what is live
|
|
1254
|
+
xanosdk workspace branch set-live staging # promote when you are happy
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
A promote **always** lands on a branch, named or not: without `--branch` the label is derived
|
|
1258
|
+
from the release and the moment (`v1-20260910T200511Z`), and the run prints it. There is no
|
|
1259
|
+
unnamed landing — a branch the engine leaves unlabelled cannot be selected, promoted or
|
|
1260
|
+
deleted afterwards, so the CLI names it rather than letting that happen.
|
|
1261
|
+
|
|
1262
|
+
`--set-live` does that last step in one go, the moment the import lands — with `--branch` to
|
|
1263
|
+
stage first and cut over immediately, or on its own as the one-shot to production:
|
|
1264
|
+
|
|
1265
|
+
```bash
|
|
1266
|
+
xanosdk promote v1 --set-live # land on a derived branch and serve it
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
The merge path takes it too, when you want the preview a release cannot run:
|
|
1270
|
+
|
|
1271
|
+
```bash
|
|
1272
|
+
xanosdk deploy ./xano/index.ts --to workspace --branch staging --dry-run # preview against the branch
|
|
1273
|
+
xanosdk deploy ./xano/index.ts --to workspace --branch staging # land it, live untouched
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
There, `--backup-branch` snapshots live first, so the way back is `xanosdk workspace branch
|
|
1277
|
+
set-live <backup-label>`. That restores **logic only**: tables are shared by every branch, so a
|
|
1278
|
+
column or table the deploy dropped stays dropped, with its data, after the switch back. The
|
|
1279
|
+
data-safe path is before landing: read `droppedColumns` in the `--dry-run` plan, and keep the
|
|
1280
|
+
column in code until its data has moved.
|
|
1281
|
+
|
|
1282
|
+
**A branch stages LOGIC, not SCHEMA.** Api groups, queries, functions, and toolsets are
|
|
1283
|
+
per-branch; **tables and microservices are shared across every branch in the workspace**. So
|
|
1284
|
+
landing a table schema change changes it for live too, branch or not — the merge path refuses
|
|
1285
|
+
that combination unless you pass `--allow-shared-schema-changes` (`SDK_SHARED_SCHEMA_CHANGE`, exit 2, as `promote`; under
|
|
1286
|
+
`--dry-run` it is reported in the plan, and `sharedSchemaChanges` counts them), because "I staged
|
|
1287
|
+
it on a branch" is exactly the belief that makes a dropped column surprising.
|
|
1288
|
+
|
|
1289
|
+
**A branch does not carry the workspace's own settings either.** The name, description,
|
|
1290
|
+
preferences, request history, realtime settings and workspace-tier middleware live on the
|
|
1291
|
+
workspace, one copy for every branch. `deploy --to workspace --branch` therefore leaves all of
|
|
1292
|
+
them as the workspace has them — `workspace("…")` does not rename the workspace from a branch —
|
|
1293
|
+
and names each one it held back (`workspaceSettingsNotApplied` under `--json`). They apply when
|
|
1294
|
+
you deploy without `--branch`, onto the live branch; after `--set-live`, that is the branch you
|
|
1295
|
+
staged. Env is the exception: a merge only ever creates a name the workspace lacks, and the staged
|
|
1296
|
+
logic reads it. A `promote` lands a release the server holds, so it keeps the name and description
|
|
1297
|
+
but applies the release's other workspace settings, and says so before it writes.
|
|
1298
|
+
|
|
1299
|
+
**Seeded ROWS are shared too, which gives reference data an order.** A table's rows are
|
|
1300
|
+
workspace-wide for the same reason its schema is, so rows reach production the moment they
|
|
1301
|
+
are applied — even while the logic that reads them is staged on a branch. No ordering makes
|
|
1302
|
+
a half-data, half-logic change atomic; one order is simply less bad than the other:
|
|
1303
|
+
|
|
1304
|
+
```bash
|
|
1305
|
+
xanosdk workspace reset-tables ./index.ts --table <guid> # dry run, per-table counts
|
|
1306
|
+
xanosdk workspace reset-tables ./index.ts --table <guid> --write # then apply
|
|
1307
|
+
xanosdk promote v2 # then the logic
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
Rows first, then logic: new rows under old logic break only a feature you are removing, while
|
|
1311
|
+
new logic over old rows breaks the one you are shipping. `reset-tables` empties each named
|
|
1312
|
+
table and re-inserts the rows your project compiles, restarting the key sequence so int ids
|
|
1313
|
+
land where the seed declares them, and touches no other table. The two calls are not one
|
|
1314
|
+
transaction — a failure between them leaves that table empty, which the command says plainly,
|
|
1315
|
+
and the remedy is to run it again, because the rows come from your source.
|
|
1316
|
+
|
|
1317
|
+
**The merge refuses rather than guessing.** It will not proceed unless the server confirms
|
|
1318
|
+
which branch it planned against. An instance whose build predates branch-targeted import
|
|
1319
|
+
fails closed instead of quietly landing on live.
|
|
1320
|
+
|
|
1321
|
+
**An unchanged project is a no-op.** Before importing, the release compares the bundle it is
|
|
1322
|
+
about to send against the workspace it is about to send it to, object by object. When every
|
|
1323
|
+
object is already there, nothing is sent: the plan prints `no changes`, the JSON summary
|
|
1324
|
+
reports `"upToDate": true` with `"operations": 0`, and no `updated_at` moves. A `--dry-run --json`
|
|
1325
|
+
carries the same `upToDate`, and lists each matched object with action `unchanged`. That makes
|
|
1326
|
+
`release` usable as a reconcile step — safe to run on a schedule, in CI on every merge, or
|
|
1327
|
+
behind a "make production match main" button.
|
|
1328
|
+
|
|
1329
|
+
**One document for a dry run and a real run.** `deploy --to … --json` writes the same keys
|
|
1330
|
+
either way, and `dryRun` says which it was:
|
|
1331
|
+
|
|
1332
|
+
| Key | Meaning |
|
|
1333
|
+
| --- | --- |
|
|
1334
|
+
| `dryRun` | `true` for `--dry-run`; nothing was written. |
|
|
1335
|
+
| `landed` | an import ran. `false` with `upToDate: true` is a success. |
|
|
1336
|
+
| `upToDate`, `operations` | already converged; how many operations were (or would be) sent. |
|
|
1337
|
+
| `mode`, `prune`, `truncate` | `merge` or `replace`, and the flags that were in force. |
|
|
1338
|
+
| `records` | rows were actually written — on a dry run, rows the real run would write. |
|
|
1339
|
+
| `workspaceId`, `destination` | where it lands (`destination.kind` `workspace`, `tenant` or `ephemeral`; a tenant's carries `label`, `url` and `display`). |
|
|
1340
|
+
| `plan`, `conflicts`, `canonicals`, `branch` | the plan it was confirmed against, identity conflicts, the public URLs served, the branch (`null` for a tenant). |
|
|
1341
|
+
| `renamedTables`, `droppedColumns`, `retypedColumns`, `narrowedEnums`, `retargetedRefs`, `indexChanges`, `notNullKept`, `notNullTightened`, `storageChanges` | the table effects (see [What a landing does to tables](#what-a-landing-does-to-tables)); `tenant deploy` and `promote` carry the same keys, plus `droppedTables` (`{ table, rows }`), under `tableEffects`. |
|
|
1342
|
+
| `unchangedEnv`, `droppedEnv`, `unappliedDocumentation`, `workspaceRename` | the rest of what the plan cannot say. |
|
|
1343
|
+
| `workspaceSettingsNotApplied` | workspace settings a `--branch` landing held back (`[]` otherwise). |
|
|
1344
|
+
|
|
1345
|
+
Present only when they apply: `branchDeletion`, `unnamedPublicUrls`, `staticRemoved`, and on a
|
|
1346
|
+
real run `setLive` and `sharedSchemaChanges` (with `--branch`), `backupBranch`, `liveRestored`
|
|
1347
|
+
and `static`.
|
|
1348
|
+
|
|
1349
|
+
The comparison is deliberately one-sided: a false "changed" costs one import that was
|
|
1350
|
+
happening anyway, while a false "unchanged" would silently skip a real release. So anything
|
|
1351
|
+
it cannot prove equal counts as changed, and it is skipped entirely for `--replace` (which
|
|
1352
|
+
rebuilds the workspace whatever it holds) and for `--seed`/`--reset-data` (which write table
|
|
1353
|
+
rows, about which the comparison knows nothing). Under `--prune`, an object the workspace holds and the project
|
|
1354
|
+
no longer defines is work to do, so that is not a no-op either.
|
|
1355
|
+
|
|
1356
|
+
On the merge path, anything destructive is **previewed first** — the CLI fetches the plan,
|
|
1357
|
+
prints what would change, and asks (an ephemeral never does). `--yes` skips the prompt for CI but never skips the
|
|
1358
|
+
preview. Start with `xanosdk deploy ./xano/index.ts --to workspace --dry-run` to see the plan
|
|
1359
|
+
without committing to it. (`promote` runs no preview — the platform lands the release — which
|
|
1360
|
+
is the trade the release flow makes and says so before writing.)
|
|
1361
|
+
|
|
1362
|
+
> ⚠️ `--prune` removes tables this project released and no longer defines, and a removed
|
|
1363
|
+
> table takes its rows with it — no flag prevents that. The preview reports it explicitly;
|
|
1364
|
+
> read it before confirming.
|
|
1365
|
+
|
|
1366
|
+
**`--prune` deletes only what this project landed on that destination.** A lock entry is
|
|
1367
|
+
not enough: guids derive from names and every export writes an entry, so two projects that
|
|
1368
|
+
both define a `double` function would each "own" the other's. Instead, `xano.lock` keeps a
|
|
1369
|
+
`landed` record per destination — `<instance host>/workspace/<id>` for a workspace,
|
|
1370
|
+
`<instance host>/tenant/<name>` for a tenant — naming each identity (lock key, guid, and
|
|
1371
|
+
whether a `toolset` entry is an agent or an MCP server) this project actually wrote there. An
|
|
1372
|
+
ephemeral's record (`<instance host>/ephemeral/<name>`) is kept in the uncommitted
|
|
1373
|
+
`.xano/ephemeral.json` instead, never the lock: ephemerals are throwaway, so recording them
|
|
1374
|
+
there churned the committed file on every new one. `ephemeral delete`, `tenant delete`, and an
|
|
1375
|
+
expired or recreated ephemeral clear its record; `tenant delete` of a standard tenant removes
|
|
1376
|
+
its entry from `xano.lock`. A planned deletion is this project's only when the record for *that* destination names
|
|
1377
|
+
its key, its guid and its kind; anything else — a table built in the UI, another project's
|
|
1378
|
+
agent of the same name, an object this project only ever exported — is refused, and nothing
|
|
1379
|
+
is sent. That refusal is a conflict, not a usage mistake: it exits `2` with
|
|
1380
|
+
`SDK_PRUNE_OUT_OF_SCOPE`, and `--yes` cannot waive it. The plan says of each object why it
|
|
1381
|
+
would go — "this project no longer declares it" only when the record shows this project landed
|
|
1382
|
+
it there and its lock entry came from this project's own source, "adopted by `lock import`, not
|
|
1383
|
+
declared by this project" when the entry only ever came from `lock import` (the lock marks such
|
|
1384
|
+
an entry `"adopted": true` until an export of this project declares it), "never declared it"
|
|
1385
|
+
when neither the record nor the lock names it. Once declared, an adopted entry is marked
|
|
1386
|
+
`"imported": true` instead: its guid is still the live object's, so an export never offers it
|
|
1387
|
+
as an orphan's new name and `lock rename` refuses to replace it.
|
|
1388
|
+
|
|
1389
|
+
What writes the record, always after the write answered (never on a dry run or a decline):
|
|
1390
|
+
|
|
1391
|
+
| Command | Record for that destination |
|
|
1392
|
+
|---|---|
|
|
1393
|
+
| `deploy --to workspace\|tenant:<x>` (merge) | adds what it sent — a converged run too |
|
|
1394
|
+
| `deploy --to … --replace` | becomes exactly what it sent |
|
|
1395
|
+
| `deploy --to … --prune` | drops what it deleted |
|
|
1396
|
+
| `deploy` to the project's ephemeral | becomes exactly what it sent (both arms leave only what was sent) |
|
|
1397
|
+
| `promote <release>` | adds the release's objects — only when every identity it carries matches this lock |
|
|
1398
|
+
| `tenant deploy`, `deploy release:<name>` | becomes the release's objects when they match this lock; cleared when they do not (said as a clear) |
|
|
1399
|
+
|
|
1400
|
+
A release deploy onto a branch counts for the whole workspace: a new branch starts as a copy of
|
|
1401
|
+
live, objects keep their guids across branches, and tables are shared by all of them. Exports
|
|
1402
|
+
never touch the record, so it never trips `--frozen-lock` or `export --check`. It is only as
|
|
1403
|
+
good as it is committed: a deploy from CI records into CI's checkout, so commit the lock back
|
|
1404
|
+
(or prune from where the record is), and a fresh clone of a lock without one prunes nothing.
|
|
1405
|
+
The refusal says so, and names the destination key. The remedy is one landing without
|
|
1406
|
+
`--prune`; an object removed from the project before its landing was ever recorded is deleted
|
|
1407
|
+
by hand, or defined again, deployed, and then removed. A prune with no lock at all has no record
|
|
1408
|
+
either, so it is the same refusal — `SDK_PRUNE_OUT_OF_SCOPE`, exit `2`, the refusal document
|
|
1409
|
+
under `--json` — saying there is no lock; reaching that usually means `--no-lock` suppressed
|
|
1410
|
+
it, and the remedy is the same one landing without `--prune` (and without `--no-lock`), then
|
|
1411
|
+
committing the lock. A lock alone does not unlock a prune: `xanosdk export` writes one with no
|
|
1412
|
+
landing record. Deploying a pre-exported `--bundle` carries no entry file to find a lock beside,
|
|
1413
|
+
so name it with `--lock=<path>` — without one that is a usage error (exit `1`). None of
|
|
1414
|
+
that applies to an ephemeral: its record is `.xano/ephemeral.json` in the project the run is in —
|
|
1415
|
+
the directory you deploy from when it is a project (it has `.xano/` or its own `xano/` entry),
|
|
1416
|
+
otherwise the entry's (`suites/alpha/.xano/` for `xanosdk deploy suites/alpha/xano/index.ts` typed
|
|
1417
|
+
from a repository root holding no project) — written lock or not, so a prune there needs no lock,
|
|
1418
|
+
and one refused there names that file and asks for a landing of that entry, with nothing to
|
|
1419
|
+
commit. The credential in `.xano/auth.json` is read from the same project. Commands that default
|
|
1420
|
+
to the tracked ephemeral read it from there too: run them from `suites/alpha`, or from a root
|
|
1421
|
+
holding no project give them the entry (`--entry=suites/alpha/xano/index.ts`). From inside a
|
|
1422
|
+
project, an entry another project owns (one below a directory with its own `xano/index.ts`,
|
|
1423
|
+
`../beta/xano/index.ts` from `suites/alpha`) is refused (exit `1`) with the command to run it
|
|
1424
|
+
from that project; a second entry of the same project, or a file no project owns, deploys, and a
|
|
1425
|
+
refresh from another entry than the one that last landed asks first (`ephemeral.entry-changed`).
|
|
1426
|
+
|
|
1427
|
+
A merge that would turn an object into a different KIND under the same identity — an MCP
|
|
1428
|
+
server onto someone's agent of the same name, which share a guid — is refused as a conflict
|
|
1429
|
+
(exit 2), naming both kinds, rather than reported as an update.
|
|
1430
|
+
|
|
1431
|
+
**A dropped column is destructive, and does not need a flag to happen.** Removing a column
|
|
1432
|
+
from a table schema and releasing destroys the column and every value in it. The server's
|
|
1433
|
+
plan calls that a routine in-place update, so the release compares your schema against the
|
|
1434
|
+
live workspace, names each column that would be dropped, and asks before doing it — on an
|
|
1435
|
+
ordinary release, with no destructive flag passed. That comparison needs the target read: a
|
|
1436
|
+
read that loses its connection or gets a server error is tried again a few times, and if it
|
|
1437
|
+
still fails a real deploy is refused before anything is written (exit 8, with the command to
|
|
1438
|
+
run again) — there is no flag to deploy without the check. `--dry-run` still prints the plan,
|
|
1439
|
+
with a warning naming why the target could not be read.
|
|
1440
|
+
|
|
1441
|
+
**Environment variables are add-only on a release.** A merge creates keys that do not exist
|
|
1442
|
+
yet and leaves existing ones as they are, so changing a value in code and releasing will not
|
|
1443
|
+
change it on the workspace. The preview names any key it will decline to update. To change
|
|
1444
|
+
one, set it on the workspace directly, or use `--replace` (which rebuilds the workspace).
|
|
1445
|
+
|
|
1446
|
+
A merge matches objects by the stable identity your project assigns them, so it only
|
|
1447
|
+
recognizes a workspace it has deployed to before. A `--replace` keeps the identities the
|
|
1448
|
+
project sends, so a workspace it rebuilt is one of those. Deploying into one built by hand
|
|
1449
|
+
matches nothing: every object is a create, and with `--prune` the workspace is emptied and rebuilt rather than updated. The
|
|
1450
|
+
preview says so in as many words when it happens. To adopt objects that are already there,
|
|
1451
|
+
pin their `guid` on the matching defs first. Deploys are **authenticated over OAuth** — sign in once,
|
|
1452
|
+
and the CLI refreshes tokens automatically. The target instance comes from your token
|
|
1453
|
+
(never a stray flag), and the CLI prints what it's about to do before it touches anything.
|
|
1454
|
+
|
|
1455
|
+
**A name the workspace already uses for a different object refuses the release.** Before the
|
|
1456
|
+
import runs, the CLI reads the live workspace and compares it to the archive by lock key. An
|
|
1457
|
+
object this project would create under a name the workspace already holds with a *different*
|
|
1458
|
+
identity is refused: the import would not update it, it would create a **second** object beside
|
|
1459
|
+
it under a suffixed name and report success — with an auth table, that binds every endpoint to
|
|
1460
|
+
one table while tokens are minted against the other. Three ways forward, all named in the
|
|
1461
|
+
message: adopt the objects that are already there with `xanosdk lock import` and commit the lock,
|
|
1462
|
+
rename yours, or release into a workspace that does not hold them. A workspace that could not be
|
|
1463
|
+
read refuses too — a failed read is not evidence that there is no collision.
|
|
1464
|
+
|
|
1465
|
+
**A public URL slug is unique across the whole instance, not per workspace.** Two workspaces on
|
|
1466
|
+
one instance cannot serve the same slug, and no flag makes them share one. A slug this project
|
|
1467
|
+
does not pin is a preference, and the import settles it: an update keeps the slug the object
|
|
1468
|
+
already had (`kept`), and a create whose slug is taken gets a random token instead (`minted`).
|
|
1469
|
+
Every slug served under a value other than the one compiled is printed, lands in the JSON
|
|
1470
|
+
summary, and **refuses `--static`** — a frontend built against the compiled value answers 404 on
|
|
1471
|
+
every route derived from it. Only the frontend publish is refused; the backend release stands. A slug pinned in code is a contract rather than a preference: the
|
|
1472
|
+
instance serves it or refuses the release — from the object's first release on, dry run
|
|
1473
|
+
included, before the lock has recorded it.
|
|
1474
|
+
|
|
1475
|
+
**A conflict refuses and exits `2`, and `--yes` cannot waive it.** `--yes` waives the
|
|
1476
|
+
confirmation, which is a question about whether you want a plan applied; a taken identity is not
|
|
1477
|
+
a question. Exit `2` is distinct from an ordinary failure's `1` on purpose — a `1` is worth
|
|
1478
|
+
retrying, and nothing about running a conflicting release again changes the answer. Nothing is
|
|
1479
|
+
written, and nothing is written back to the lock. When the holder is this workspace, every
|
|
1480
|
+
remedy is in your own project: pin the identity that is already there so the release updates it
|
|
1481
|
+
(`guid: "<it>"` on the def — a def's explicit guid wins over the lock's entry, and the next build
|
|
1482
|
+
re-pins the lock and says so; the message offers this first, because it takes over that one
|
|
1483
|
+
object), rename the object, or adopt everything the workspace holds — `lock import` takes an
|
|
1484
|
+
exported bundle, so the message names the export that writes one (`workspace export`, or
|
|
1485
|
+
`ephemeral export <name> --format json` for an ephemeral) and then `lock import … --yes`: the
|
|
1486
|
+
import overwrites the identity the lock pins, which is the adoption you chose. It adopts **every**
|
|
1487
|
+
identity in that file, not only the one that conflicted: each becomes this project's, and once a
|
|
1488
|
+
deploy or release records it as landed, a `--prune` from this project can delete it. Run
|
|
1489
|
+
without `--yes`, `lock import` lists what it would overwrite and asks on a terminal; with no
|
|
1490
|
+
terminal it refuses and names the flag. A standard tenant cannot be
|
|
1491
|
+
exported, so there the remedies are the pin (the guid is in the message) or a rename. When the holder
|
|
1492
|
+
is **another workspace on the instance, no remedy in this project applies** — the message names
|
|
1493
|
+
that workspace by id and says the fix lives there. On `--to tenant:<name>` the refusal names the
|
|
1494
|
+
tenant or ephemeral you chose (with its display name), never its internal workspace. A refusal
|
|
1495
|
+
at apply time reports that the destination was not changed, and names any backup branch taken
|
|
1496
|
+
moments earlier as redundant rather than as a rollback target.
|
|
1497
|
+
|
|
1498
|
+
Under `--json` every refusal — a taken identity, a kind swap, a prune out of scope, or one at
|
|
1499
|
+
apply time — writes the failure document every command writes: `{ ok: false, error: { code,
|
|
1500
|
+
message, exitCode, details } }`. `error.code` names the refusal — `SDK_IDENTITY_CONFLICT`,
|
|
1501
|
+
`SDK_KIND_CONFLICT`, `SDK_PRUNE_OUT_OF_SCOPE`, or `SDK_IMPORT_REFUSED` for one at apply time
|
|
1502
|
+
(`SDK_IDENTITY_CONFLICT` when the instance's reason was a taken identity) — and `error.exitCode`
|
|
1503
|
+
is `2`, or `1` for an apply-time refusal that is not a conflict. `error.details` carries the
|
|
1504
|
+
refusal itself: `refused` (`identityConflict`, `kindConflict`, `pruneOutOfScope` or
|
|
1505
|
+
`importRefused`), `landed` (`false`), `conflicts`, `kindConflicts`, `outOfScope` (each object as
|
|
1506
|
+
`{ type, name, label, reason? }`), `canonicals` and `landingRecord` (`null`) — each always
|
|
1507
|
+
present on a `deploy --to` refusal, a list empty when it does not apply — plus the dry run's
|
|
1508
|
+
`dryRun`, `declined`, `workspaceId`, `destination`, `mode`, `prune` and `branch` once the
|
|
1509
|
+
destination was read, and `serverCode`, the instance's own code, on a refusal at apply time.
|
|
1510
|
+
A `--keep-data` merge's refusal is smaller: `refused` (`identityConflict`, `renamePending`,
|
|
1511
|
+
`pruneOutOfScope`, `storageModeChange`, `tableTrigger` or `uniqueViolation`), `conflicts`, `renames` and `outOfScope`, plus
|
|
1512
|
+
`serverCode` when the environment refused — [above](#keeping-your-data-between-deploys).
|
|
1513
|
+
|
|
1514
|
+
**CI & agents** run headless with the meta credential — `XANO_INSTANCE_URL`, `XANO_WORKSPACE_ID`
|
|
1515
|
+
and `XANO_META_TOKEN`, [above](#signing-in--deploying-in-depth) — no browser, nothing on disk,
|
|
1516
|
+
nothing that rotates:
|
|
1517
|
+
|
|
1518
|
+
```bash
|
|
1519
|
+
XANO_INSTANCE_URL=… XANO_WORKSPACE_ID=… XANO_META_TOKEN=… npx xanosdk deploy ./xano/index.ts
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
> ⚠️ A deploy without `--keep-data` is a full replace of the target environment, including
|
|
1523
|
+
> its table records, before importing. The blast radius is your own disposable ephemeral — but anything
|
|
1524
|
+
> you only ever created by hand in it (or any data it accumulated) is gone. That's exactly
|
|
1525
|
+
> why a real workspace or tenant is reached only by naming it, with `deploy --to`.
|