# Put it in a repository

> The app directory holds a live Chatfuel token, so the first push is a secret question before it is a git question. What the wizard's GitHub step checks, how to do it by hand, and how to make git push the deploy command.

Page: https://sdk.chatfuel.com/docs/guides/push-to-github
Markdown: https://sdk.chatfuel.com/docs/guides/push-to-github.md

The wizard writes an ordinary React + Vite project and then leaves. Deploying it needs no
repository — `npm run deploy` works from a bare directory and always will — so the code can be
live and still sitting in an un-versioned folder on one laptop. This is the separate question.

It is a secret question before it is a git question. The app directory holds a live Chatfuel
token, and that token can read and change every bot in the account it belongs to. `.gitignore`
covers `.env`, but a `.gitignore` can be declined, a file of your own can appear between then and
now, and a wrong answer here is a token on the internet.

<Steps>
  <Step>
    ### Check what would be committed [#check-what-would-be-committed]

    The scaffold ships with this:

    ```text title=".gitignore"
    node_modules/
    dist/
    .env
    .env.*
    !.env.example
    .DS_Store

    # Vercel CLI project link (created by `npm run deploy`).
    .vercel/
    ```

    `.env.example` is the one `.env*` meant to be published — it holds the names of the variables and
    none of the values. Before the first push, read the index rather than the ignore file:

    ```bash
    git add -A
    git ls-files --cached | grep -E '(^|/)\.env($|\.)|(^|/)\.vercel/|\.pem$|(^|/)id_(rsa|ed25519)'
    ```

    Nothing back means nothing forbidden is staged. Then check by content as well as by name, because
    a token can be pasted into a file that is not called `.env`:

    ```bash
    TOKEN=$(grep '^CHATFUEL_TOKEN=' .env | cut -d= -f2-)
    [ -n "$TOKEN" ] && git grep --cached -l -F -e "$TOKEN"
    ```

    That is the pair of checks the wizard runs itself: staged paths matched by name — every `.env`
    except `.env.example`, `*.pem`, `id_rsa*`, `id_ed25519*`, and anything under `.vercel/` — and the
    staged contents searched for the token and the Supabase secret key. The Supabase anon key and the
    workspace id are deliberately not on that list: both are shipped to the browser by design, and a
    gate that cries wolf gets switched off.
  </Step>

  <Step>
    ### Push it [#push-it]

    <Tabs items="['The wizard', 'By hand']">
      <Tab value="The wizard">
        Say yes to &#x2A;*Put this app on GitHub?** — a question the run asks after the deploy and before the
        agent handoff, because `gh auth login` prints a one-time code and waits for a browser, and after
        the handoff an agent session may already own the terminal.

        It never runs in `--yes` or `--dry-run`, and never without a TTY. A repository under somebody's
        GitHub account, holding the source of an app wired to their bot, is not something a
        non-interactive flag should be able to cause.

        What happens then: it asks for a name and rewrites it the way GitHub would have silently (letters,
        digits, dot, dash and underscore), asks private or public, and prepares the local repository —
        `git init`, `HEAD` on `refs/heads/main`, everything staged, the two secret scans above, and a first
        commit. A directory that is already inside another repository is left alone; one that already has
        an `origin` is reported rather than taken over. If the scan finds something, the `.git` it created
        a moment ago is removed again and the step stops: you asked for your app on GitHub, not for a
        half-made git directory to understand before you can retry.

        For the GitHub side it wants `gh`. Already installed is free; otherwise it offers Homebrew on
        macOS, winget or Scoop on Windows, and the official release archive everywhere else — the only
        route on Linux, because every distro package manager there needs `sudo` and the wizard escalates
        privileges on nobody's behalf. Every route ends by running the binary and seeing if it answers.
        If there is no working `gh` at all, it asks for a GitHub token instead and uses the API. That
        fallback is for the absence of a `gh`, not for a `gh` that tried and failed: a name already taken
        fails identically on both paths.
      </Tab>

      <Tab value="By hand">
        This is the command the wizard prints when you decline, and the one it runs itself:

        ```bash
        cd chatfuel-app
        git init
        git symbolic-ref HEAD refs/heads/main
        git add -A
        git commit -m "Initial commit"
        gh repo create <name> --source . --remote origin --push --private
        ```

        `gh` authenticates the push it makes itself, so this works even on a machine whose git has no
        github.com credentials.

        Nothing here scans for secrets. That is the trade: run the two checks from the previous step
        before `git commit`, every time, and treat the first push to a public repository as irreversible.
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### Make a push deploy [#make-a-push-deploy]

    Once a repository exists, connecting it means `git push` and `npm run deploy` do the same thing
    and you can stop remembering which:

    ```bash
    npm run connect-git
    ```

    Two things must already be true, and the script checks both before it calls the Vercel CLI,
    because Vercel's own error for either is a menu you did not expect. The project must be linked —
    `.vercel/project.json`, written by `npm run deploy` — and there must be an `origin` remote. An SSH
    remote or one carrying a username names the same repository in a form the CLI does not accept, so
    the script rewrites it to a plain `https://host/owner/repo` first.

    Re-running is safe: `vercel git connect` on an already-connected project is a no-op that says so.

    The commonest failure is not this app's: a Vercel account with no GitHub connection at all, which
    only you can grant, in a browser. The script prints Vercel's own words first and then the two
    places to fix it.

    The wizard offers this step itself, but only when the deploy step already linked a project —
    without one there is nothing to connect the repository *to*.
  </Step>

  <Step>
    ### Verify [#verify]

    `npm run connect-git` prints &#x2A;*Connected — pushing to the default branch now deploys to
    production.** Prove it rather than believe it. Change something committed that you can see from
    the outside — a heading in one of your modules, a token in `src/vendor/ui/styles/tokens.css`, the
    logo file in `public/` — then:

    ```bash
    git add -A && git commit -m "A change I can see" && git push
    ```

    Watch the deployment appear on Vercel and open the production URL. If nothing happens, the
    repository is not connected and `npm run connect-git` says why.

    Then check the repository itself for the thing that matters most:

    ```bash
    gh api repos/{owner}/{repo}/contents/.env --jq .name
    ```

    A `404` is the answer you want. Anything else means `.env` is in the repository and you are on the
    callout below.
  </Step>
</Steps>

<Callout type="warn">
  A token that has landed in a commit is public from that moment, and rewriting history does not
  un-publish it — anything that fetched the repository in between still has it. Rotate it: generate
  a new token, then run `npx @chatfuel/wizard auth` in the app directory, which rewrites the
  `CHATFUEL_TOKEN` line in `.env` and nothing else. Push the new value to your deployment with
  `npm run deploy`, which overwrites the variable rather than adding a second one. Then remove the
  file from the repository and fix the `.gitignore` that let it through.
</Callout>

## What to read next [#what-to-read-next]

[Rotate the token](/docs/guides/rotate-the-token) is the full procedure for the callout above.
[Vercel](/docs/deploy/vercel) covers what `npm run deploy` does and what it pushes;
[scripts](/docs/reference/scripts) lists every script in the app, `connect-git` included.
