# Spatibase quickstart for coding agents

You are helping a developer move the location data that lives in their repo (JSON, GeoJSON, CSV files, or arrays with coordinates in code) into Spatibase, a CMS and API for places, and rewire their app to read it from the Spatibase API. Follow the steps in order. Talk to the developer in plain words; they may not know Spatibase.

Version: 2026-10-30. This file changes over time; read it fresh at the start of each task. Human-readable docs: https://spatibase.com/docs/build/start-from-your-code/

## 0. Rules
- **Change nothing before the developer's one OK (step 2).** After it, do steps 3 to 7 without asking again unless something unexpected comes up.
- **The tool is the `spatibase` CLI.** Run it as `npx --yes spatibase@0.1 <command>` (Node 18.3 or newer; it has no dependencies). Below, `spatibase …` always means that full command. If `npx` is not available, use the appendix.
- **Project state lives in `.spatibase/`** in the repo root. The CLI creates it with a `.gitignore` containing `*`, so nothing in it is ever committed. It holds `project.json` (id, addresses and the secret token). Put any helper script or model file you write inside `.spatibase/` too.
- **Never print the token, never copy it elsewhere** (not into tracked files, not into your messages). The CLI reads it from `.spatibase/project.json` by itself; you never need it.
- Large data files go from disk into the CLI. Do not paste them through your own context.
- Every command prints a short plain result; failed ones say what to do next. Read the message, do not retry the same call unchanged. Exit code 0 is ok, 1 failed, 2 wrong usage. `--json` prints one JSON object if you need to parse the result.
- `spatibase <command> --help` shows every option.

## 1. Scan the repo
Look for location data:
- Files: `*.geojson`, `*.json`, `*.csv`, `*.tsv` whose records have coordinates (`lat`/`lng`, `latitude`/`longitude`, `coordinates`, `"type": "Feature"`) or street addresses.
- Code: arrays or objects with coordinates in source files (`const stores = [{ name, lat, lng }]`).
- Skip `node_modules`, build output (`dist`, `.next`, `build`), lock files and test fixtures.

Useful: `rg -l --hidden -g '!node_modules' -g '!dist' -g '!.next' -e '"lat(itude)?"\s*:' -e '"(lng|lon|longitude)"\s*:' -e '"type"\s*:\s*"Feature'`

For each source note: the file, how many records, the fields and their value types, the id field if the records have one, and where the app reads it (the import or fetch, and the components using it).

**Several languages:** if the repo has one file per language (`stores.en.json`, `stores.de.json`, `locales/…`), treat them as one set of places with translations (step 4, "Languages"). Find which language is the default (the one with every record).

Nothing found: say so and offer to design the data with the developer from a description instead (you can still create the project and model it).

## 2. Report and ask for one OK
Show one short report:
1. **Found:** files, record counts, fields, languages.
2. **Content model:** one Type per kind of record (for example *Store*) with its fields and their kinds (table in section 8), which property is the title, which field is the identifier.
3. **Code change:** the files you will change and how (for example "`StoreList.tsx` fetches from the API instead of importing `stores.json`; the old file stays until you delete it").
4. **Visibility:** "The data will be public but unlisted: readable only by someone who knows the API address, not linked from anywhere, and not indexed by search engines. If that is not OK, I can have you sign up first and make the API private."
5. **Temporary:** "Spatibase keeps this project for 24 hours. At the end I will ask for your email so you can keep it."

Then wait for the OK. If the data looks private (people's emails or phone numbers, internal notes), recommend the sign-up-first route (section 9) in the report.

## 3. Create the temporary project
```
spatibase create "<app name> places" --ref agent-quickstart
```
It needs no account and no browser. It prints the project id, the **Delivery API address** (public, read-only, this is what the app will fetch), the expiry (24 hours) and the limits (**1,000 places** and **50 MB** of uploaded files). Already have a project in `.spatibase/`? `create` refuses; add `--new` only if the developer wants a fresh one.

## 4. Model, import, publish, verify
**Model.** Write the content model as a small JSON file, for example `.spatibase/model.json`, then run it. It is safe to run again (existing apiKeys are skipped).
```
{ "types": [ { "name": "Store", "apiKey": "store", "locationMode": "required", "geometryKinds": ["Point"],
    "fields": [ { "label": "Store id", "apiKey": "store_id", "kind": "identifier" },
                { "label": "Phone", "apiKey": "phone", "kind": "phone" },
                { "label": "Category", "apiKey": "category", "kind": "choice", "config": { "options": ["Cafe", "Shop"] } },
                { "label": "Photos", "apiKey": "photos", "kind": "images" } ] } ] }
```
```
spatibase model .spatibase/model.json          # add --preview to see what would be created
spatibase describe                             # the model as created: use these apiKeys
```
- `apiKey` rule: lowercase letters, digits, `-` and `_`, starting with a letter. The title and the coordinates are not fields: every entry has a title, and a Type with `locationMode: "required"` places each entry on the map.
- Put parent Types before children. Fields are shared between Types by apiKey.
- Add an `identifier` field when the data has ids; it lets you re-run an import without duplicates (`--match-by`).

**Import.** One command per file and Type. It reads GeoJSON, CSV, TSV or a JSON array of objects, sends 1,000 rows at a time as drafts and, with `--publish`, makes them live in the same run.
```
spatibase import public/stores.json --type store --dry-run                       # check first: counts and warnings, nothing written
spatibase import public/stores.json --type store --match-by store_id --publish
```
- Latitude and longitude: columns named `lat`/`latitude` and `lng`/`lon`/`long`/`longitude` (any case) or a nested `location`/`coordinates` object are found automatically. Coordinates outside range, or swapped, give a warning: read the warnings and fix the data.
- `--title <prop>` names the property that holds the title (default `name`, `title` or `label`). `--map field=prop` fills a field from a differently named property (repeat it). Properties already named like a field apiKey need no `--map`.
- **Images.** `--images <prop>[=<dir>]` names a property that holds image file paths (`/images/01.webp`). The CLI uploads each distinct file once (up to 3.5 MB each; larger ones are skipped with a warning) from `<dir>` (default `./public` if it exists, else `.`) and fills the Type's `images` field with them. The Type needs a field of kind `images` with that apiKey (or `--map photos=<prop>`). Remote `http(s)` URLs stay as they are (use a `link` field for those instead). Uploads count against the 50 MB limit.
- **Languages.** If the repo has one file per language: import the default language first as above, with `--match-by` on an identifier field. Then each other language, matched to the same entries:
  ```
  spatibase call update_project_settings '{"locales":["en","de","es"]}'     # default language first; needs the model tier, which a temporary project has
  spatibase import i18n/stores.de.json --type store --match-by store_id --locale de
  ```
  A translation import only fills the title and the `text` and `markdown` fields, and creates nothing: rows without a match are skipped with a warning. Translations are published with the entries: publish again afterwards (`spatibase publish`). The Delivery API then answers `?locale=de`, and `Accept-Language` also picks the language. See https://spatibase.com/docs/content/translations/. Do not add `name_de`, `name_es` fields.
- Temporary projects hold 1,000 places. If the source has more, the CLI sends the first 1,000 and warns; tell the developer.
- Anything the CLI cannot read (a JS array in code, XML): write a few lines of Node inside `.spatibase/` that turns it into a GeoJSON or JSON file, then import that.

**Publish and verify.**
```
spatibase publish        # only needed if you imported without --publish; entries that fail validation stay drafts and are listed
spatibase verify         # compares live Delivery counts with the project; exit code 1 when something is not live
```
Compare the counts with the source. Look at one entry: `curl -s "<delivery address>/entries?limit=1"`.

## 5. Rewire the app
- Replace the local import or read with a fetch of the Delivery API, in the app's own style (React effect or hook, Next.js server component, Astro frontmatter, plain `fetch`). The Delivery API is read-only, public and open to browsers (CORS `*`), no key.
- Endpoints, relative to the Delivery address from `create`: `/entries.geojson` (a GeoJSON FeatureCollection, up to 1,000 per request by default, good for maps), `/entries` (JSON, `limit` up to 500, `page`), `/entries/<id>`, `/types`, filters such as `?type=store&q=…&filter[<apiKey>]=…`, `?locale=de` for translations. Field values are under `fields` keyed by the field apiKey (GeoJSON: in `properties`). Image fields are lists of `{url, width, height, alt…}`; add `?w=800&fmt=webp` to a URL to resize. Reference: https://spatibase.com/docs/reference/delivery-api.md
- TypeScript app: the typed OpenAPI file of the project is at `<delivery address>/openapi.json`; generate types from it instead of guessing field names.
- Put the Delivery address in one config constant or environment variable (`NEXT_PUBLIC_…`, `VITE_…`, whatever the app uses). Keep loading and error states.
- Keep the old data file; tell the developer they can delete it.
- Static or pre-built sites fetch at build time: say that publishing a change in Spatibase then needs a rebuild. Published changes reach other clients within about a minute (cached).
- Run the app's own checks (lint, type check, tests, build) and fix what you broke.

## 6. Keep the project: ask for the email
Ask: "Spatibase deletes this temporary project in 24 hours unless you keep it. What email should I send the link to?" Use only an address the developer types for this purpose.
```
spatibase keep dev@example.com
spatibase status        # claim state, expiry; "claimed" once they clicked the link
```
They receive "Keep your project". One click creates their account (or signs them in) and the project becomes theirs: **same project id, same Delivery address, so the rewired code keeps working**. Giving an email extends the project to 72 hours, with two reminder emails. At most 3 different addresses per project.

## 7. Finish
Tell the developer, in short:
- the live API address the app now uses,
- what changed in the code (files, and which old files they can delete),
- the number of places, Types and uploaded images, and any warnings (skipped rows, files too large, the 1,000 limit),
- when the project expires, and that one click in the email keeps it; then their team edits the places at https://app.spatibase.com.

Optional, for future sessions: connecting the assistant to the project's MCP address (`mcpUrl` in `spatibase create --json` output) works after they have kept the project (https://spatibase.com/docs/ai/connect-an-assistant/).

An expired project answers **410** `project_expired` ("This temporary project expired because nobody kept it"). If that happens, tell the developer the project is gone and offer to run the quickstart again.

A **401** `token_revoked` means the project's owner removed your access in the app. Stop, tell the developer, and ask what they want. Never create a new project to get around it.

## 8. Field kinds
| Source value | Kind | Notes |
|---|---|---|
| name, short text, address line | `text` | the title is not a field |
| long text, HTML or markdown | `markdown` | |
| number (price, rating, capacity) | `number` | `config`: `min`, `max`, `integer`, `unit` |
| true or false | `boolean` | `yes/no/1/0/true/false` are read |
| date or datetime | `date` | `config.mode`: `date`, `datetime`, `time` |
| one of a few fixed values (category) | `choice` | `config.options`: list of strings or `{value,label}`; `config.multiple` for several |
| URL, website | `link` | `{url, label?}`, `http(s)` only |
| phone | `phone` | 5 to 25 characters |
| email, or a block with name, email, phone, website, address | `contact` | |
| id, slug, code | `identifier` | unique per project; use it with `--match-by` |
| hex colour | `color` | `#rrggbb` |
| image file paths in the repo | `images` | import with `--images`; uploads are stored in the project |
| other files (PDF, GPX) | `files` | `spatibase call upload_asset --args-file <file>` with `{filename, contentBase64}` (up to 3.5 MB each) |
| link to another place (the region of a store) | `reference` | by id or by the title of the target; import the targets first |
| nested object or list (opening hours by day) | `text` with `"config": {"editor": "json"}` | the object is stored as JSON and delivered parsed; keep it in a JSON or GeoJSON source file |
| the coordinates | the Type's location | not a field |

Fields you cannot place cleanly: use `text` or `markdown` and mention it in the report rather than dropping data.

## 9. Sign-up-first route (private data)
Use it when the data should not be readable by anyone who knows the URL, or when the developer prefers an account first.
1. `spatibase create "<name>" --ref agent-quickstart` (still empty).
2. Ask for the email now and run `spatibase keep <email>`. Tell the developer to click the link in the email.
3. Poll `spatibase status` about every 5 seconds until it says the project is claimed. Do not import anything before that.
4. Tell the developer to open the project in the app (`App:` address from `create`), choose **API**, then **Access & keys**, and make the API **private (key only)**, then create a read key (**For a website or app**) and put it in the environment variable the app will use (for example `.env.local`). You cannot change access or create keys yourself, and you must never ask them to paste the key into the chat. Check that the env file is ignored by git (`.gitignore`).
5. Continue with step 4 (model, import, publish, verify), then rewire the app (step 5) so it sends the key from the environment, from server-side code only, never in browser code or in a URL (https://spatibase.com/docs/reference/authentication.md).
6. The temporary token lasts 24 hours from `create`. After the claim the developer's own account owns the project; if the token ran out, ask them to connect the assistant (https://spatibase.com/docs/ai/connect-an-assistant/) or run the quickstart's steps in a new session.

## Appendix: without `npx` (curl and JSON-RPC)
Use only when Node is not available. Everything the CLI does is an HTTP call.

**Create the project** (no account, no auth):
```
mkdir -p .spatibase && echo '*' > .spatibase/.gitignore
curl -sS -X POST https://api.spatibase.com/api/agent/projects -H 'Content-Type: application/json' \
  -d '{"name":"<app name> places","ref":"agent-quickstart"}' > .spatibase/project.json
jq 'del(.token)' .spatibase/project.json      # projectId, mcpUrl, deliveryUrl, appUrl, expiresAt, limits
```
Read the token inside each command (`jq -r .token .spatibase/project.json`), because shell variables do not survive between your commands. Never print it.

**Call a tool** (the MCP endpoint is plain stateless JSON over HTTP; no setup, no `initialize`, no session):
```
call() {  # call <tool> '<arguments json>'
  jq -n --arg n "$1" --argjson a "$2" '{jsonrpc:"2.0",id:1,method:"tools/call",params:{name:$n,arguments:$a}}' \
  | curl -sS -X POST "$(jq -r .mcpUrl .spatibase/project.json)" \
      -H "Authorization: Bearer $(jq -r .token .spatibase/project.json)" \
      -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' --data-binary @- \
  | jq -r '.result.content[0].text // .'
}
call describe_project '{}'     # start here; do not print tools/list (35 KB)
```
The answer is a JSON **string** in `.result.content[0].text`; a failed tool sets `.result.isError` and its text says what to fix. A wrong argument type comes back as `MCP error -32602: …`.

Two safety switches run in opposite directions; forgetting one silently does nothing:
- model tools (`create_element`, `create_type`, `assign_elements`, `update_project_settings`) only preview unless `"confirm": true`. Send it: the developer approved the model.
- `import_features` and `publish_entries` only dry-run unless `"dryRun": false`.

**Order:**
1. `call create_element '{"label":"Phone","apiKey":"phone","kind":"phone","confirm":true}'` for each field.
2. `call create_type '{"name":"Store","apiKey":"store","locationMode":"required","geometryKinds":["Point"],"fields":["phone"],"confirm":true}'`
3. Import, at most **1,000 rows per call**, built from the file on disk so the data never passes through your context (`publish: true` makes the rows live in the same call and needs the publish capability, which a temporary token has):
   ```
   jq -n --slurpfile d stores.geojson '{jsonrpc:"2.0",id:1,method:"tools/call",params:{name:"import_features",
     arguments:{type:"store",format:"geojson",data:$d[0],mapping:{title:"name",elements:{phone:"tel"}},matchBy:"store_id",publish:true,dryRun:false}}}' \
   | curl -sS -X POST "$(jq -r .mcpUrl .spatibase/project.json)" -H "Authorization: Bearer $(jq -r .token .spatibase/project.json)" \
       -H 'Content-Type: application/json' --data-binary @- | jq -r '.result.content[0].text'
   ```
   Do one call with `dryRun: true` first and read the counts and warnings. The answer lists `ids` (row order, `null` for a skipped row), `published` and `publishFailed`. With more than 1,000 rows, slice the file with `jq '.features |= .[0:1000]'` and so on. Translations: `locale: "de"` with `matchBy`.
4. Images: `call upload_asset '{"filename":"a.webp","contentBase64":"…"}'` (up to 3.5 MB decoded) returns `{id,…}`; give the entry `["<id>"]` in its `images` field.
5. Verify: `curl -s "<deliveryUrl>/entries.geojson" | jq '.features|length'` equals the source count.
6. Keep: `curl -sS -X POST "https://api.spatibase.com/api/agent/projects/<projectId>/claim-email" -H "Authorization: Bearer <token from the file>" -H 'Content-Type: application/json' -d '{"email":"dev@example.com"}'`, and status with `GET https://api.spatibase.com/api/agent/projects/<projectId>` (same Bearer token): `claimState` is `unclaimed` or `claimed`.
