Quickstart: from sign-up to a published URL

Audience: a coding agent working for a human, plus the few minutes of that human's time this path cannot avoid. The account belongs to a person, so a person creates it; everything after that is yours and is verifiable from a terminal.

Everything below was checked against dev on 24/09/2026. Times are what the steps take there, not promises.

0. What the human does once (a few minutes, in a browser)

  1. Open https://dev.infra.doublex.ai.
  2. Click Get started (Começar in Portuguese) and create the account with email and password, or Google. The organization is created on the first sign-in, on the free Hobby plan (1 GiB of lake, 3 end users, 1 environment).
  3. Land on /welcome and copy the command shown there. It looks like:
claude mcp add --scope user --transport http cortex https://mcp.dev.infra.doublex.ai/mcp

--scope user matters: it registers the server once for every folder. Without it Claude Code creates a local-scope entry for the current folder, and a local entry wins over the user one.

Ask for that command, or for the screen it is on. Do not ask for a password, and do not ask the human to paste a token into the chat: the browser flow below never needs one.

1. Connect the MCP server

Run the command. On first use Claude Code opens the browser, the human signs in with the same account as the site, and the client stores the session.

Verify: call whoami. It answers with the organization, the plan and your scopes.

If it goes wrong:

SymptomWhat to do
cortex is listed but has no tools, or asks for sign-inIn Claude Code type /mcp, choose cortex, then Authenticate, with the same account as the site
it points to localhost or another wrong URLclaude mcp remove cortex -s local (and -s user if needed), then add it again with the command from /welcome
the browser says Authentication failed … invalid_targeta server setting, fixed on 24/09/2026; if it comes back, send the full URL from the address bar to the Cortex team

2. Install and sign in the CLI

deploy runs in the terminal, not in the MCP server, so the terminal needs the companion CLI.

The cortex command. It is not on npm, and npx cortex installs an unrelated package with the same name: never run it. Build it from the platform checkout with make cli, which leaves cortex in ~/.local/bin (put that on the PATH). cortex --version proves it is ours. It talks to dev by default.

make cli                # in the platform checkout
cortex login            # opens the browser
cortex whoami --json    # subject, org (slug, name, plan), scopes, api

cortex login finds the authorization server on its own, registers a public client, stores the credential in ~/.cortex/credentials.json with mode 0600, and refreshes it before it expires. If the browser cannot open, create a token on the console's Tokens page and run cortex login --pat <token>; in CI put the token in CORTEX_TOKEN instead.

Verify: cortex whoami --json exits 0. Exit code 3 means there is no usable credential: run cortex login again.

3. Create the project

Agree on the slug with the human first: it is global.

Ask the human for the region; never choose it for them. region is required and permanent: the lake and the app run there (southamerica-east1 for Brazil, us-central1 for the United States). In dev today the runtime exists in us-central1; a region without a runtime is refused by create_project with an error that names the regions that work. Say so to the human and ask again; never retry in another region on your own.

create_project        { slug: "acme-bi", name: "Acme BI", region: "us-central1" }
get_project           { project_id }        # repeat until status is "active"
apply_project_config  { project_id, yaml }  # a minimal cortex.yaml: version and project

The project starts as provisioning and turns active within a few minutes. Apply a configuration right after that, even a minimal one: today the catalog syncs on the first apply, and run_sql before it answers that the catalog is not synced yet.

version: 1
project: { slug: acme-bi, name: Acme BI, region: us-central1 }

4. Load data

A spreadsheet or a CSV file:

upload_file      { project_id, filename: "sales.xlsx" }   # returns upload_id and a signed url
# PUT the bytes to that url (for example with curl -X PUT --upload-file)
finalize_upload  { upload_id }                            # returns a run_id
get_run          { run_id }                               # until status is "succeeded"

A workbook becomes one table per sheet, named raw.<file>_<sheet>; get_run lists the real names in tables_touched.

The files are on the human's disk? cortex upload sales.xlsx data/ --json runs those four calls for every file and waits for the loads. Or keep them for the first deploy (step 6, cortex deploy --with-data), which uploads them before the app.

A database reachable from the internet (Postgres, MySQL, SQL Server):

set_secret    { project_id, name: "ERP_DSN", value }   # the value never goes to a file
add_source    { project_id, name: "erp", type: "database", connector: "postgres", secret: "ERP_DSN" }
test_source   { project_id, source: "erp" }

Then declare a pipeline for that source under pipelines in cortex.yaml, apply it with apply_project_config, and run it with run_pipeline { project_id, pipeline: "<name>" } and get_run. A database inside the human's network is not reachable from the cloud today.

Verify: list_tables { project_id } returns the tables, and sample_table { project_id, table } shows rows the human recognizes. If the numbers are wrong here, stop: nothing downstream will fix them.

5. Model, and define the metrics

propose_model       { project_id }         # returns a change set; it applies nothing
get_change_set      { change_set_id }      # read it, explain it to the human
approve_change_set  { change_set_id }      # asks the human, with the diff
run_models          { project_id }

Metrics live in semantic.metrics of cortex.yaml: add them and apply with apply_project_config. Then:

query_metrics  { project_id, query: { metrics: ["revenue"], dimensions: ["orders.channel"] } }

Verify: query_metrics returns numbers and the SQL that produced them. Show both to the human. A number without SQL is a bug, not an answer.

6. Publish the app

The project must already exist and be active (step 3), and --slug must be its slug.

cortex init bi-nextjs --dir acme-bi --slug acme-bi --region us-central1
cd acme-bi
cortex deploy --json     # pack, upload, Cloud Build, Cloud Run; prints the URL

deploy honours .gitignore and .cortexignore and refuses .env*. If the answer is approval.required, the environment is protected: show the change set to the human, approve it with approve_change_set, and run cortex deploy again.

The first deploy of a slug takes a few minutes more after live: the certificate of the new hostname is being issued. deploy waits for the URL to answer over HTTPS (up to 6 minutes) before it prints it. If it prints a warning instead, the deploy is fine: wait a minute and retry.

First deploy and local data. On the first deploy of a project (no deployment in any environment yet), the human decides whether the data files on their disk go up with the app. Ask them before running the command, or call create_deployment { project_id }: it asks them and answers with the command to run, cortex deploy --with-data or cortex deploy.

  • cortex deploy --with-data --json uploads every local data file first (CSV, TSV, Excel .xlsx, Parquet), waits until each one is loaded, then deploys the app. The files are the type: files sources of cortex.yaml whose path exists on disk; without those, every data file in the folder, .gitignore and .cortexignore applied. Files that went up as data are left out of the app package (--keep-data-in-app ships them too).
  • --data <path|glob>[,...] picks the files (.gitignore does not apply to it); --no-data deploys only the app. --env pr<N> sends the data to that preview.
  • With no flag and no terminal (you, an agent), the command never asks: it deploys only the app and prints a --with-data hint on stderr. --json reports data.decision, data.uploads and data.run_ids.
  • A data failure stops before the app is deployed: exit 7 when an upload is refused, exit 1 with run.failed when a load fails. Read get_run_logs { run_id }, fix the file, run again.
  • .xls is not read: save it as .xlsx.
  • Never uploaded as data, whatever the flag or glob: .env*; keys and credential files (*service-account*.json, *credentials*.json, *.pem, *.key, *.p12, *.pfx, id_rsa*, id_ed25519*, .npmrc, .pypirc); text files whose first 4 KiB hold a private key or a service-account JSON; symbolic links that resolve outside the folder (a link met by a glob or a folder walk is never followed). Each one prints a warning: line on stderr and is listed in refused (data.refused for deploy) with its reason. Named on its own, the command exits 7. Globs skip package.json, tsconfig*.json and other tooling JSON.

Today the template's dashboard shows sample data: end-user login works, but the numbers do not come from the lake yet. Say so to the human.

Verify: open https://acme-bi.apps.dev.infra.doublex.ai; /healthz answers {"status":"ok"}. If the build fails, read get_build_logs { deployment_id }; to go back, rollback_deployment { project_id }.

7. Tell the human what exists now

  • The URL, and who can sign in to it (invite them with invite_end_user { project_id, email, roles }).
  • The metrics you defined, in their words, with the SQL behind one of them.
  • What the dashboard shows today (sample data) and what still has to be wired.
  • Whether the app is the right size: get_app_metrics { project_id, env: "production" } after some traffic (see "Is my app the right size?" in Bootstrap a project).

When something fails

SymptomWhat it meansWhat to do
cortex exits 3no credential, or it expiredcortex login, or cortex login --pat <token>
cortex: command not found, or npx cortex prints an unrelated toolthe companion is not installed; the npm name is someone else'smake cli in the platform checkout; never npx cortex
init exits 7 saying the packages are not builtthe CLI was not built with its bundlemake cli, then init again (nothing was written)
deploy says the project was not foundproject.slug in cortex.yaml is not an existing projectcreate_project first, or fix the slug, or pass --project <id>
deploy exits with approval.requiredthe environment is protectedapprove_change_set with the human, then deploy again
the URL fails the TLS handshake right after a deploythe certificate of a new hostname is still being issuedwait; deploy already waited up to 6 min and said so
deploy --with-data exits 7 with "found no local data file"the data is in .gitignore, or it is .xls--data <folder>, or a type: files source with a local path; save .xls as .xlsx
upload or deploy --data exits 7 saying a file "looks like a credential" or "is a symbolic link"the file is a key, a credential or a link out of the folder; the companion never uploads those as dataupload the real data file; copy a linked file into the folder; never work around it
deploy --with-data exits 1 with run.faileda file did not load; the app was not deployedget_run_logs { run_id }, fix the file, run again (--no-data deploys only the app)
cortex exits 9the command exists but is not implemented yet (ingest, dev, config)use the MCP tools above instead
query_metrics refuses a metricit is not in the semantic modelget_semantic_model, then define it; never invent SQL for a number

Every command takes --json and every error carries code, message and hint. Branch on the exit code, not on the text.