> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Shipping workflow

> Every change gets a live preview. Every release is tested once and shipped unchanged to dev, stage, and prod. Rolling back is one command.

export const ShippingFigure = ({name, alt}) => <div className="not-prose my-8">
    <div className="hidden sm:block">
      <img className="block dark:hidden w-full" src={`/images/shipping/${name}-desktop-light.svg`} alt={alt} />
      <img className="hidden dark:block w-full" src={`/images/shipping/${name}-desktop-dark.svg`} alt={alt} />
    </div>
    <div className="block sm:hidden">
      <img className="block dark:hidden w-full" src={`/images/shipping/${name}-mobile-light.svg`} alt={alt} />
      <img className="hidden dark:block w-full" src={`/images/shipping/${name}-mobile-dark.svg`} alt={alt} />
    </div>
  </div>;

This is how a change gets from your editor to production. The steps are the same whether you
work alone or on a team, and each one is a single command.

<ShippingFigure name="flow" alt="Each change goes from a git branch to its own preview, through a pull request, and into main. A release is cut once, tagged v1.4.0, and that same release is shipped to dev, stage, and prod." />

<Columns cols={3}>
  <Card title="A preview for every change" icon="eye">
    Each branch gets its own live copy of your backend. Nothing you try there can touch anyone
    else.
  </Card>

  <Card title="Test once, ship everywhere" icon="box">
    A release is a saved, numbered version of your backend. The release that passed stage is
    the exact one that reaches prod.
  </Card>

  <Card title="Undo in one command" icon="rotate-left">
    Production always runs a release. To go back, ship the one before it.
  </Card>
</Columns>

**Git is the source of truth.** Your code lives in a GitHub repo, and `main` holds what
production runs or is about to run. If your team builds in the Xano builder instead, the same
flow works: see [If Xano is your source of truth](#if-xano-is-your-source-of-truth).

## Where your backend runs

<ShippingFigure name="environments" alt="With a tenant stack, every change gets an ephemeral, and releases go to dev, stage, and prod tenants, each with its own database. Without tenants, ephemerals work the same and your workspace is production." />

| Place | What it is |
| - | - |
| **Ephemeral** | A temporary copy of your backend with its own URL. Every change gets one, and it expires on its own (after an hour, by default). |
| **Tenant** | A permanent environment with its own database. Teams usually keep three: dev, stage, and prod. |
| **Workspace** | Your Xano workspace. It stores your releases, and if you do not use tenants, it is production. |

Everything below is the same with or without tenants. Only the last step changes.

## Set up once

<Tabs>
  <Tab title="A new project">
    ```bash theme={null}
    npx @xano/sdk login                     # pick your instance and workspace
    npx @xano/sdk init my-app && cd my-app
    git init && git add -A && git commit -m "scaffold"
    ```
  </Tab>

  <Tab title="A workspace that already exists">
    ```bash theme={null}
    npx @xano/sdk init my-app --from workspace
    cd my-app && git init && git add -A && git commit -m "adopt workspace"
    ```

    <Warning>
      Commit `xano/xano.lock` with that first commit. `init --from workspace` writes it with the
      identities of the objects already in the workspace, which links your code to them. Without
      it, the first release creates a copy of every object instead of updating it. More in
      [Start from an existing workspace](/start/existing-workspace).
    </Warning>
  </Tab>
</Tabs>

Push the repo to GitHub. On a team, protect `main` so changes only arrive through pull
requests.

Using tenants? Create them in the Xano dashboard, then give each one its own secrets. `env set`
reads the value from a file or a pipe, so it never appears on screen:

```bash theme={null}
xanosdk tenant list                                   # each tenant's name, in bold
xanosdk env set STRIPE_KEY --to tenant:prod --yes < prod-stripe.key
```

## The workflow

<Steps>
  <Step title="Branch, and get a preview">
    ```bash theme={null}
    git switch -c orders-api
    # edit xano/…
    xanosdk deploy ./xano/index.ts --ephemeral --static ./frontend/dist --test
    ```

    `deploy --ephemeral` puts your branch on your own ephemeral, runs your tests, and prints the
    URL. It is yours alone, so deploy to it as often as you like. (A bare `deploy` goes to the
    Xano Engine on your machine, which nobody else can open.)
  </Step>

  <Step title="Open a pull request">
    ```bash theme={null}
    git add -A && git commit -m "add orders API"
    git push -u origin orders-api
    gh pr create --fill
    ```

    CI gives the pull request its own ephemeral too, so reviewers click through a running app
    instead of only reading a diff. They also check [four things specific to the
    Xano SDK](/govern/git-and-merges#reviewing-a-xano-sdk-pull-request).
  </Step>

  <Step title="Merge to main">
    Merge once it is approved. Keep `main` releasable: anything merged there can ship.
  </Step>

  <Step title="Cut a release">
    ```bash theme={null}
    git switch main && git pull
    xanosdk deploy ./xano/index.ts --ephemeral --test     # main, on an ephemeral, with its tests
    xanosdk release create v1.4.0 --from ephemeral        # save what just passed as v1.4.0
    git tag v1.4.0 && git push origin v1.4.0
    ```

    A release never changes once it is created, and giving it the same name as the git tag
    means every release points back to its exact code. To change anything, cut `v1.4.1`. Cut
    from the ephemeral, not the Xano Engine
    ([why](/run/xano-engine#where-the-engine-differs-today)). With
    [CI set up](#automate-it-with-github-actions), pushing the tag does all of this for you.
  </Step>

  <Step title="Ship it">
    <Tabs>
      <Tab title="With tenants">
        ```bash theme={null}
        xanosdk tenant deploy dev   v1.4.0
        xanosdk tenant deploy stage v1.4.0
        xanosdk tenant deploy prod  v1.4.0
        ```

        Check each one before moving to the next. Shipping a frontend too? Build it once from
        the tag and publish the same build to each tenant. It picks up that tenant's URL on
        its own:

        ```bash theme={null}
        xanosdk publish ./frontend/dist --to tenant:prod --yes
        ```

        <Warning>
          **A tenant deploy makes the tenant match the release exactly.** Anything not in the
          release is removed, including tables and the data in them. Before you ship a release
          that removes or changes a table, back up prod in the Xano dashboard and preview the
          change:

          ```bash theme={null}
          git diff v1.3.2 v1.4.0 -- xano/                             # what changed
          xanosdk deploy ./xano/index.ts --to tenant:prod --dry-run   # columns it drops; changes nothing
          ```
        </Warning>
      </Tab>

      <Tab title="With the workspace as production">
        ```bash theme={null}
        xanosdk deploy ./xano/index.ts --to workspace --dry-run   # preview, if a table changed
        xanosdk promote v1.4.0 --set-live
        xanosdk publish ./frontend/dist --to workspace --yes
        ```

        `promote` puts the release live on a fresh Xano branch and keeps the previous one, so
        you can switch back. It never deletes an object.

        <Warning>
          **Table changes reach production as soon as the release lands.** Every Xano branch
          shares the same tables, and `promote` shows no preview. It refuses a release that
          alters a table (exit 2, `SDK_SHARED_SCHEMA_CHANGE`) until you pass
          `--allow-shared-schema-changes`. Before you do, run the
          `deploy --to workspace --dry-run` above: it lists every column the release would drop.
          See [Schema changes](/govern/schema-changes).
        </Warning>
      </Tab>
    </Tabs>

    Each landing is recorded in `xano/xano.lock`. Commit the lock afterwards, so the next
    `deploy --to … --prune` knows what this project put there.
  </Step>
</Steps>

## Solo or team

The steps are the same. A team adds review and approval.

| | Solo | Team |
| - | - | - |
| Branches | Recommended, so each change gets its own preview | Required |
| Pull requests | Optional | Required, with `main` protected |
| Cutting releases | You | One release owner, or CI on every tag |
| Shipping to prod | You | An approval step: a GitHub environment with required reviewers |

## When something goes wrong

<ShippingFigure name="rollback" alt="Prod was running v1.4.0, which turned out to be bad. Deploying v1.3.2 to prod puts the previous release back. The fix then ships as v1.4.1." />

Production always runs a release, so going back means serving the previous one:

```bash theme={null}
xanosdk tenant deploy prod v1.3.2                      # with tenants
xanosdk workspace branch set-live <previous branch>    # with the workspace as production
```

With the workspace, `<previous branch>` is the Xano branch that was live before the promote
(`xanosdk workspace branch list` shows them). Table changes are not rolled back by either
command: see [Rollback and recovery](/govern/rollback-and-recovery).

Then fix the problem on `main` and ship `v1.4.1`. Until `main` is fixed, the next release
brings the problem back.

<Warning>
  A rollback restores your logic, not your data. If the bad release dropped a table or a column,
  it comes back empty. Your backup is how you recover the rows.
</Warning>

**Hotfixes follow the same steps:** branch, review, merge, release `v1.4.1`. This is why `main`
stays releasable. If it holds unfinished work, branch from the tag instead
(`git switch -c hotfix v1.4.0`) and merge the fix back into `main` afterwards.

**Do not edit production by hand.** A tenant deploy replaces anything edited directly. To look
around safely, open a tenant read-only with `xanosdk impersonate tenant:prod --guest`.

**Made an emergency fix in the Xano builder?** With the workspace as production, that is
sometimes the fastest option. Bring the fix into git the same day, before the next release
overwrites it, then open a pull request:

```bash theme={null}
git switch -c builder-fix
xanosdk pull workspace         # replaces xano/ with what the workspace runs
git diff                       # should show only the fix
```

## If Xano is your source of truth

<ShippingFigure name="paths" alt="Two ways to author, one way to ship. With git as the source of truth, changes go from a git branch through a pull request into main. With Xano as the source of truth, changes go from a Xano branch through review in the builder to the live branch, and you sync git at each release. Both produce a release that ships to the same environments." />

Some teams build in the Xano builder rather than in code. You get the same releases,
environments, and rollbacks. Two things change: you make and review changes in the workspace,
and **you keep git in sync yourself**.

| Step | Git is the source of truth | Xano is the source of truth |
| - | - | - |
| Start a change | `git switch -c orders-api` | Create a Xano branch for the change |
| Review | A pull request | Click through the Xano branch |
| Merge | Merge into `main` | `xanosdk workspace branch set-live orders-api` |
| Release | `release create v1.4.0`, then tag it | `release create v1.4.0 --from workspace`, then sync git |
| Ship and roll back | The same commands | The same commands |

Five practices keep this safe:

1. **Never edit the live Xano branch.** Make each change on its own Xano branch and serve it
   once it is reviewed. Without tenants, serving it is also your production release.

2. **Keep Xano branches small and short-lived.** There is no merge tool: if two people change
   the same object on different Xano branches, the one served second replaces the other's work.

3. **Treat table changes with care.** Every Xano branch shares the same tables, so adding or
   dropping a column changes the live workspace right away.

4. **Sync git at every release.** Git becomes your history and your backup:

   ```bash theme={null}
   xanosdk release create v1.4.0 --from workspace
   git switch main && git pull
   xanosdk pull release:v1.4.0
   git add -A && git commit -m "v1.4.0"
   git tag v1.4.0 && git push origin main v1.4.0
   ```

5. **Never push from git to the workspace.** Running `promote` or `deploy --to workspace` from
   the repo makes git a second source of truth, and someone's work gets overwritten. To switch
   to git as the source of truth later, start at [Set up once](#set-up-once).

## Automate it with GitHub Actions

With this in place, every pull request gets its own preview, pushing a tag releases to dev,
and one click ships to stage or prod.

CI signs in with three repository secrets instead of `xanosdk login`: `XANO_INSTANCE_URL`,
`XANO_WORKSPACE_ID`, and `XANO_META_TOKEN` (see
[Environment variables](/reference/environment-variables)).

<Accordion title=".github/workflows/xano.yml" icon="github">
  ```yaml theme={null}
  name: xano
  on:
    pull_request:
    push:
      tags: ["v*"]
    workflow_dispatch:
      inputs:
        release:
          description: "Release to ship, e.g. v1.4.0"
          required: true
        environment:
          type: choice
          options: [stage, prod]

  jobs:
    # Every pull request: build it, test it on its own ephemeral, leave a live URL.
    preview:
      if: github.event_name == 'pull_request'
      runs-on: ubuntu-latest
      env:
        XANO_INSTANCE_URL: ${{ secrets.XANO_INSTANCE_URL }}
        XANO_WORKSPACE_ID: ${{ secrets.XANO_WORKSPACE_ID }}
        XANO_META_TOKEN: ${{ secrets.XANO_META_TOKEN }}
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: 20, cache: npm }
        - run: npm ci
        - run: npm run xano:check
        - run: npm run build
        - name: Write xano/.env
          run: printf '%s\n' "$XANO_BACKEND_ENV" > xano/.env
          env: { XANO_BACKEND_ENV: "${{ secrets.XANO_BACKEND_ENV }}" }
        - run: npx xanosdk deploy ./xano/index.ts --ephemeral --static ./frontend/dist --test

    # A pushed tag: test main on an ephemeral, cut the release from it, ship it to dev.
    release:
      if: startsWith(github.ref, 'refs/tags/v')
      runs-on: ubuntu-latest
      concurrency:
        group: xano-release
        cancel-in-progress: false
      permissions: { contents: write, pull-requests: write }
      env:
        RELEASE: ${{ github.ref_name }}
        TARGET: dev
        XANO_INSTANCE_URL: ${{ secrets.XANO_INSTANCE_URL }}
        XANO_WORKSPACE_ID: ${{ secrets.XANO_WORKSPACE_ID }}
        XANO_META_TOKEN: ${{ secrets.XANO_META_TOKEN }}
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: 20, cache: npm }
        - run: npm ci && npm run build
        - name: Write xano/.env
          run: printf '%s\n' "$XANO_BACKEND_ENV" > xano/.env
          env: { XANO_BACKEND_ENV: "${{ secrets.XANO_BACKEND_ENV }}" }
        - run: npx xanosdk deploy ./xano/index.ts --ephemeral --test --frozen-lock
        - run: npx xanosdk release create "$RELEASE" --from ephemeral
        - run: npx xanosdk tenant deploy dev "$RELEASE" --yes
        - run: npx xanosdk publish ./frontend/dist --to tenant:dev --yes
        # The landing is recorded in xano/xano.lock. Send it back to main as a pull request.
        - name: Commit the landing record
          if: ${{ !cancelled() }}
          env: { GH_TOKEN: "${{ github.token }}" }
          run: |
            git diff --quiet -- xano/xano.lock && exit 0
            git config user.name "github-actions[bot]"
            git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git switch -c "xano-lock/$RELEASE-$TARGET"
            git commit -m "xano.lock: $RELEASE landed on $TARGET" -- xano/xano.lock
            git push origin HEAD
            gh pr create --base main --fill

    # Run from the Actions tab: ship an existing release to stage or prod.
    ship:
      if: github.event_name == 'workflow_dispatch'
      runs-on: ubuntu-latest
      environment: ${{ inputs.environment }}   # add required reviewers to prod
      concurrency:
        group: xano-release
        cancel-in-progress: false
      permissions: { contents: write, pull-requests: write }
      env:
        RELEASE: ${{ inputs.release }}
        TARGET: ${{ inputs.environment }}
        XANO_INSTANCE_URL: ${{ secrets.XANO_INSTANCE_URL }}
        XANO_WORKSPACE_ID: ${{ secrets.XANO_WORKSPACE_ID }}
        XANO_META_TOKEN: ${{ secrets.XANO_META_TOKEN }}
      steps:
        - uses: actions/checkout@v4
          with: { ref: "${{ inputs.release }}" }
        - uses: actions/setup-node@v4
          with: { node-version: 20, cache: npm }
        - run: npm ci && npm run build
        - run: npx xanosdk tenant deploy "$TARGET" "$RELEASE" --yes
        - run: npx xanosdk publish ./frontend/dist --to "tenant:$TARGET" --yes
        - name: Commit the landing record
          if: ${{ !cancelled() }}
          env: { GH_TOKEN: "${{ github.token }}" }
          run: |
            git diff --quiet -- xano/xano.lock && exit 0
            git config user.name "github-actions[bot]"
            git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
            git switch -c "xano-lock/$RELEASE-$TARGET"
            git commit -m "xano.lock: $RELEASE landed on $TARGET" -- xano/xano.lock
            git push origin HEAD
            gh pr create --base main --fill
  ```
</Accordion>

* **A failing test stops a tag from becoming a release.** Delete the tag
  (`git push --delete origin v1.4.0`), fix `main`, and tag again.
* **A forgotten `xano/xano.lock` fails the run before anything lands.** On a pull request,
  `npm run xano:check` stops the preview before it deploys. It runs
  `xanosdk export ./xano/index.ts --check --strict`, which implies `--frozen-lock` and needs no
  secrets. On a tag, the release deploy passes `--frozen-lock` itself.
* **The landing comes back as a pull request.** A tenant deploy or promote records what it
  landed in `xano/xano.lock`, in CI's checkout. The last step of each landing job opens a pull
  request with that change; merge it like any other. It needs "Allow GitHub Actions to create
  and approve pull requests" turned on in the repository's Actions settings.
* **Never re-run a step that exited 9.** Exit 9 means the write was sent and the answer was
  lost, so it may have landed. A retried `release create` can leave a second release under a
  suffixed name. Run the `resolveWith` command the step printed, or check with
  `xanosdk release list` and `xanosdk tenant get <tenant>`, before anything else. See
  [Exit codes and outcomes](/help/exit-codes-and-outcomes).
* **No tenants?** In `release`, drop the two lines that ship to dev. In `ship`, run
  `npx xanosdk promote "$RELEASE" --set-live --yes` and publish `--to workspace`. A release that
  alters a table is refused (exit 2, `SDK_SHARED_SCHEMA_CHANGE`) until you add
  `--allow-shared-schema-changes`; read [Schema changes](/govern/schema-changes) before you put
  that flag in a workflow.

## The rules

1. **One source of truth.** Git by default, Xano if your team builds there, never both.
2. **Every change gets a branch and a preview.**
3. **Only releases reach production, and the same release goes everywhere.** Environments differ
   by their secrets, never by their code.
4. **Roll back by shipping the previous release.** Back up first when a release changes a table.
5. **Commit `xano/xano.lock`. Never commit secrets.**

## Where to go next

<CardGroup cols={2}>
  <Card title="Git, locks & merges" icon="code-merge" href="/govern/git-and-merges">
    What to commit, resolving a `xano.lock` conflict, renames, and the review checklist.
  </Card>

  <Card title="Releases" icon="rocket" href="/ship/releases">
    The details underneath: cutting, listing, and landing releases, and Xano branches.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.