Aito — the predictive database, in a container

Pull, run, predict.

From docker pull to your first prediction in five minutes: an HTTP API on port 9005 and SQL over the Postgres wire protocol on port 5432.

docker pull ghcr.io/aitohq/aito

export AITO_KEY=$(openssl rand -hex 24)        # read-write key: keep it
export AITO_READ_KEY=$(openssl rand -hex 24)   # read-only key
docker run -d --name aito \
  -p 127.0.0.1:9005:9005 \
  -p 127.0.0.1:5432:5432 \
  -v aito-state:/io/state \
  -e READ_WRITE_APIKEY="$AITO_KEY" \
  -e APIKEY="${AITO_READ_KEY:?set AITO_READ_KEY first}" \
  ghcr.io/aitohq/aito

The 127.0.0.1: prefix keeps both ports on your own machine. Docker publishes ports past host firewalls such as ufw, so leave it in unless you mean to expose the instance.

Your API keys

The container does not run unauthenticated. It uses two API keys: the read-write key (READ_WRITE_APIKEY) can insert and change data, and the read-only key (APIKEY) can query but not INSERT, UPDATE, DELETE or DROP. Give the read-only one to anything that only reads.

Pin your own keys, as above, for anything you intend to keep running, and hold them wherever you already hold secrets. Pinned keys are used as-is, and nothing is generated.

If you start without keys, the container generates a pair on first boot and stores it on the aito-state volume, where later restarts reuse it. The startup log shows the pair on first boot only, so read it from the volume:

# while the container exists
docker exec aito cat /io/state/.aito-api-keys

# even if the container is gone: the volume is enough
docker run --rm -v aito-state:/io/state alpine cat /io/state/.aito-api-keys

For a throwaway local container you can opt out entirely with -e AITO_DISABLE_AUTH=true. Do not do that on a published port.

Upgrading from 1.0.1? Two things change:

  • Authentication is on. 1.0.1 ran without keys. Every HTTP request now needs an x-api-key header, and SQL connections use the key as the password. Pin your own keys, or read the generated pair from the volume as above.
  • AITO_LICENSE_KEY works. In 1.0.1, setting it left the container running with nothing listening. If you tried a licence key against 1.0.1 and it looked broken, it was.

From pull to prediction

Three requests: create a table, insert rows, ask for a prediction. The example predicts how an incoming purchase invoice should be coded, the same pattern behind predictive accounting and ERP automation.

1. Create the table. One collection of purchase invoices. The description column is analyzed as text, so Aito can learn from individual words:

curl -X PUT http://127.0.0.1:9005/api/v2/schema/invoices \
  -H "x-api-key: ${AITO_KEY:?set AITO_KEY first}" \
  -H 'content-type: application/json' \
  -d '{
  "type": "collection",
  "columns": {
    "vendor":      { "type": "String" },
    "description": { "type": "Text", "analyzer": "english" },
    "amount":      { "type": "Decimal" },
    "gl_account":  { "type": "String" },
    "approver":    { "type": "String" }
  }
}'

2. Insert rows. Twelve invoices that have already been coded, the kind of history every accounting system has:

curl -X POST http://127.0.0.1:9005/api/v2/data/invoices/batch \
  -H "x-api-key: ${AITO_KEY:?set AITO_KEY first}" \
  -H 'content-type: application/json' \
  -d '[
  { "vendor": "AWS", "description": "Monthly cloud hosting and compute", "amount": 2340.50, "gl_account": "6510 Cloud services", "approver": "sara" },
  { "vendor": "Google Cloud", "description": "Cloud compute and BigQuery usage", "amount": 1480.00, "gl_account": "6510 Cloud services", "approver": "sara" },
  { "vendor": "Slack", "description": "Team messaging subscription, 25 seats", "amount": 218.75, "gl_account": "6520 Software subscriptions", "approver": "sara" },
  { "vendor": "Atlassian", "description": "Jira and Confluence annual subscription", "amount": 1890.00, "gl_account": "6520 Software subscriptions", "approver": "sara" },
  { "vendor": "Finnair", "description": "Flights Helsinki to Berlin, customer visit", "amount": 420.30, "gl_account": "6310 Travel", "approver": "tom" },
  { "vendor": "Scandic Hotels", "description": "Hotel two nights, conference trip", "amount": 358.00, "gl_account": "6310 Travel", "approver": "tom" },
  { "vendor": "Lyreco", "description": "Office paper, pens and notebooks", "amount": 86.40, "gl_account": "6110 Office supplies", "approver": "tom" },
  { "vendor": "IKEA", "description": "Office chairs and desk lamps", "amount": 540.00, "gl_account": "6110 Office supplies", "approver": "tom" },
  { "vendor": "Google Ads", "description": "Search advertising campaign April", "amount": 1500.00, "gl_account": "6610 Marketing", "approver": "lisa" },
  { "vendor": "LinkedIn", "description": "Sponsored content campaign, lead gen", "amount": 980.00, "gl_account": "6610 Marketing", "approver": "lisa" },
  { "vendor": "Wolt", "description": "Team lunch, quarterly planning day", "amount": 156.80, "gl_account": "6210 Staff catering", "approver": "tom" },
  { "vendor": "Kespro", "description": "Office coffee, milk and snacks", "amount": 92.30, "gl_account": "6210 Staff catering", "approver": "tom" }
]'

3. Ask for a prediction. A new invoice arrives from Datadog, a vendor that appears nowhere in the data:

curl -X POST http://127.0.0.1:9005/api/v2/_predict \
  -H "x-api-key: ${AITO_KEY:?set AITO_KEY first}" \
  -H 'content-type: application/json' \
  -d '{
  "from": "invoices",
  "where": { "vendor": "Datadog", "description": "Monitoring subscription for production servers" },
  "predict": "gl_account",
  "select": ["$value", "$p", "$why"],
  "limit": 3
}'
{
  "offset": 0,
  "total": 6,
  "hits": [
    { "$p": 0.281, "$value": "6520 Software subscriptions", "$why": { … } },
    { "$p": 0.144, "$value": "6110 Office supplies", "$why": { … } },
    { "$p": 0.144, "$value": "6210 Staff catering", "$why": { … } }
  ]
}

Aito has never seen Datadog, but the word "subscription" in the description carries the prediction toward the software account, and $why shows which words and values moved it. Change "predict": "gl_account" to "predict": "approver" and the same rows suggest who should approve it.

There is no training step: every prediction is computed live from the rows you just inserted, and the next query already includes any rows you add.

The same thing in SQL

Port 5432 speaks the Postgres wire protocol, so psql, a BI tool or any Postgres driver connects directly. The password is the API key:

PGPASSWORD="${AITO_KEY:?set AITO_KEY first}" psql -h 127.0.0.1 -p 5432 -U aito -d aito
SELECT value, p FROM predict('invoices', 'gl_account',
  where => 'vendor = ''Datadog'' AND description = ''Monitoring subscription for production servers''');

If a local Postgres already holds port 5432, docker run fails to publish it. Map Aito's SQL port to another host port instead, for example -p 127.0.0.1:15432:5432, and connect with psql -h 127.0.0.1 -p 15432.

The examples use 127.0.0.1 rather than localhost: the ports are published on IPv4, and some systems (macOS among them) resolve localhost to IPv6 first.

See the SQL guide for predict(), recommend() and relate() inside SELECT.

To start over, remove the container and the volume (docker rm -f aito && docker volume rm aito-state). Removing only the container keeps both your data and your keys.

What else you can query

QueryWhat it does
_predictPredict a field's value from the other fields, with a calibrated probability
_recommendRank the values of a field toward a goal
_relateFind the conditions that go with an outcome, ranked by lift
_matchMatch a record to the rows of a linked table
_searchFull-text and structured search
_queryPlain filtered reads, with links followed by name
_evaluateMeasure prediction accuracy on your own held-out rows

v2 quickstart → · API reference →

Free for development. Licensed for production.

The free-tier image holds up to 10,000 rows per table and 50,000 rows total. Beyond that, inserts return HTTP 429. Reads keep working at any size — the cap only stops new data.

That covers most evaluation, CI, and prototyping work. For production deployments — anything that serves your customers or participates in a revenue workflow — you'll want a Production License.

What counts as "production"?

If your Aito instance is part of any of the following, it's production:

  • Serving data to your customers, end-users, or employees
  • A pipeline that generates revenue or supports a revenue process
  • An internal tool at a company with more than 250 employees or annual revenue above $10M
  • An internet-facing endpoint with real users

Not production: local dev, CI, side projects, personal use, academic research, evaluation up to 90 days. All free.

Get a Production License

Email sales@aito.ai with a one-line description of your use case. You'll get a license key back within a working day — no qualifying call, no SDR follow-up. Founder-led, by design.

Read the full license terms →

How it works in production

Once you have a Production License key, set it as an environment variable and the row caps lift:

docker run -d --name aito \
  -p 127.0.0.1:9005:9005 \
  -p 127.0.0.1:5432:5432 \
  -v aito-state:/io/state \
  -e READ_WRITE_APIKEY="${AITO_KEY:?set AITO_KEY first}" \
  -e APIKEY="${AITO_READ_KEY:?set AITO_READ_KEY first}" \
  -e AITO_LICENSE_KEY=ak_live_... \
  ghcr.io/aitohq/aito:latest

The image validates the licence key against console.aito.ai on startup and caches the response, encrypted, on the /io/state volume, so the container keeps running for up to 7 days without internet if you need offline operation. If validation cannot complete, the server starts in free mode and says so in its log rather than refusing to start.

Configuration

VariableDefaultMeaning
READ_WRITE_APIKEYgeneratedFull-access key; set it to pin your own
APIKEYgeneratedRead-only key; set it to pin your own
AITO_DISABLE_AUTHfalsetrue disables all authentication. Local use only
AITO_LICENSE_KEYunsetProduction licence key
AITO_LICENSE_TIMEOUT5Seconds per licence validation request
AITO_LICENSE_STARTUP_TIMEOUT60Seconds to wait for validation at startup before starting in free mode
AITO_LICENSE_CACHE_MAX_AGE_SECONDS604800 (7 days)How long a cached validation is honoured offline
JVM_XMX / JVM_XMS2g / 512mJVM heap
PORT / PGWIRE_PORT9005 / 5432HTTP and Postgres wire ports
STATE_PATH/io/stateWhere the database, keys and licence cache persist

What we collect

Every Aito instance phones home on startup with:

  • A random instance UUID
  • Software version, row count, table count
  • Your IP address
  • Java/OS version

We do not collect: row contents, query text, schemas, table data. The telemetry is how we tell free-tier evaluation traffic from production traffic — and it's how sales knows to reach out about a license. Full details in our privacy policy under "Free-tier Docker image telemetry".

Disabling telemetry is a breach of the license terms §4(b).

Cloud-hosted Aito

Prefer a managed instance? console.aito.ai runs Aito for you with the same query interface, plus backups and the Aito UI on top. The Sandbox tier is free; Dev and Prod tiers are paid. The cloud service is governed by separate Terms of Service.

Mirrors

The image is also published to AWS ECR Public for users in AWS-native environments:

docker pull public.ecr.aws/aitoai/aito

Identical image, identical contents.


Questions: support@aito.ai · GitHub · License terms