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)
- Open
https://dev.infra.doublex.ai. - Click Get started (
Começarin 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). - Land on
/welcomeand 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:
| Symptom | What to do |
|---|---|
cortex is listed but has no tools, or asks for sign-in | In Claude Code type /mcp, choose cortex, then Authenticate, with the same account as the site |
it points to localhost or another wrong URL | claude 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_target | a 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
cortexcommand. It is not on npm, andnpx cortexinstalls an unrelated package with the same name: never run it. Build it from the platform checkout withmake cli, which leavescortexin~/.local/bin(put that on thePATH).cortex --versionproves it is ours. It talks todevby default.
make cli # in the platform checkout
cortex login # opens the browser
cortex whoami --json # subject, org (slug, name, plan), scopes, apicortex 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 projectThe 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 URLdeploy 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 --jsonuploads every local data file first (CSV, TSV, Excel.xlsx, Parquet), waits until each one is loaded, then deploys the app. The files are thetype: filessources ofcortex.yamlwhosepathexists on disk; without those, every data file in the folder,.gitignoreand.cortexignoreapplied. Files that went up as data are left out of the app package (--keep-data-in-appships them too).--data <path|glob>[,...]picks the files (.gitignoredoes not apply to it);--no-datadeploys 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-datahint on stderr.--jsonreportsdata.decision,data.uploadsanddata.run_ids. - A data failure stops before the app is deployed: exit 7 when an upload is refused, exit 1 with
run.failedwhen a load fails. Readget_run_logs { run_id }, fix the file, run again. .xlsis 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 awarning:line on stderr and is listed inrefused(data.refusedfordeploy) with itsreason. Named on its own, the command exits 7. Globs skippackage.json,tsconfig*.jsonand 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
| Symptom | What it means | What to do |
|---|---|---|
cortex exits 3 | no credential, or it expired | cortex login, or cortex login --pat <token> |
cortex: command not found, or npx cortex prints an unrelated tool | the companion is not installed; the npm name is someone else's | make cli in the platform checkout; never npx cortex |
init exits 7 saying the packages are not built | the CLI was not built with its bundle | make cli, then init again (nothing was written) |
deploy says the project was not found | project.slug in cortex.yaml is not an existing project | create_project first, or fix the slug, or pass --project <id> |
deploy exits with approval.required | the environment is protected | approve_change_set with the human, then deploy again |
| the URL fails the TLS handshake right after a deploy | the certificate of a new hostname is still being issued | wait; 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 data | upload the real data file; copy a linked file into the folder; never work around it |
deploy --with-data exits 1 with run.failed | a file did not load; the app was not deployed | get_run_logs { run_id }, fix the file, run again (--no-data deploys only the app) |
cortex exits 9 | the command exists but is not implemented yet (ingest, dev, config) | use the MCP tools above instead |
query_metrics refuses a metric | it is not in the semantic model | get_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.