# ApiQL v3 Agent Guide

This file is written for AI coding agents, automation agents and deployment agents that need to install, configure, use or integrate ApiQL v3 without asking the user for missing operational details.

Public documentation:

- Human documentation: https://apiql.net/documentation/v3/
- Short documentation URL: https://apiql.net/v3/
- Agent guide download: https://apiql.net/documentation/v3/downloads/AGENTS.md
- Installer: https://apiql.net/documentation/v3/downloads/install.sh
- Global JavaScript SDK: https://apiql.net/v3/sdk/apiql-min.js

## What ApiQL v3 is

ApiQL v3 is a standalone Go service that exposes MySQL tables and views through two transports:

- REST for normal CRUD requests.
- WebSocket for realtime events and WebSocket CRUD requests.

Both transports use the same:

- JSON configuration file.
- MySQL connection.
- token authentication.
- permission rules.
- query builder.
- CRUD implementation.
- response format.

Users normally receive only the compiled binary and the external JSON config. Do not assume the user has Go source code on the target server.

## Important agent rules

Follow these rules when operating on a user server:

1. Do not rebuild the Go binary just to change configuration.
2. Edit `/etc/apiql-v3/config.json` for runtime configuration changes.
3. Restart the service after every config change.
4. Do not print real tokens, database passwords or production credentials in logs, comments, commits or chat output.
5. Prefer `tokens_table` for production systems because it gives each token a `user_id`, `role`, `status`, `created_at` and `last_used_at`.
6. Use `allow_all: true` only for trusted internal APIs.
7. Keep dangerous or sensitive columns in `disabled_columns` unless the user explicitly wants unrestricted internal access.
8. Use the generated runtime documentation at `/_documentation` as the live schema reference for that specific installation.
9. When building SQL manually outside ApiQL, always quote MySQL identifiers with backticks because table or column names may be reserved words such as `cascade`.
10. Before reporting success, verify the service with `_health` and at least one authenticated request.

## Installation

Basic install:

```bash
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo bash
```

Install with database values:

```bash
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo env \
APIQL_DB_HOST=localhost \
APIQL_DB_NAME=my_database \
APIQL_DB_USER=my_user \
APIQL_DB_PASS='my_password' \
APIQL_TOKEN='change_this_token' \
bash
```

Install with a custom starting port:

```bash
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo env \
APIQL_PORT=8105 \
APIQL_DB_HOST=localhost \
APIQL_DB_NAME=my_database \
APIQL_DB_USER=my_user \
APIQL_DB_PASS='my_password' \
APIQL_TOKEN='change_this_token' \
bash
```

Install with a full listen address:

```bash
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo env \
APIQL_LISTEN=127.0.0.1:8105 \
APIQL_DB_NAME=my_database \
APIQL_DB_USER=my_user \
APIQL_DB_PASS='my_password' \
APIQL_TOKEN='change_this_token' \
bash
```

Install with Apache reverse proxy setup:

```bash
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo env \
APIQL_DOMAIN=api.example.com \
APIQL_EMAIL=admin@example.com \
APIQL_DB_NAME=my_database \
APIQL_DB_USER=my_user \
APIQL_DB_PASS='my_password' \
APIQL_TOKEN='change_this_token' \
bash
```

The installer starts from `127.0.0.1:8098`. If the port is already in use and no existing config is present, it selects the next free port automatically and writes that address to the generated config.

## Installed files

Default paths:

```text
/usr/local/bin/apiql-v3
/opt/apiql-v3/
/etc/apiql-v3/config.json
/etc/systemd/system/apiql-v3.service
```

Service commands:

```bash
sudo systemctl status apiql-v3 --no-pager
sudo systemctl enable --now apiql-v3
sudo systemctl restart apiql-v3
sudo journalctl -u apiql-v3 -n 100 --no-pager
sudo journalctl -u apiql-v3 -f
```

Edit config:

```bash
sudo nano /etc/apiql-v3/config.json
sudo systemctl restart apiql-v3
```

## Health and live documentation

Local health:

```bash
curl -fsSL http://127.0.0.1:8098/_health
```

Public health:

```bash
curl -fsSL https://api.example.com/_health
```

Generated live documentation:

```text
http://127.0.0.1:8098/_documentation
https://api.example.com/_documentation
```

The generated documentation is built from the active JSON config and current MySQL schema. It also includes a Try Live API console where a user or agent can test endpoints with a token and inspect full JSON responses.

## Minimal config

```json
{
  "listen": "127.0.0.1:8098",
  "app_name": "ApiQL v3 project",
  "app_desc": "Standalone REST and WebSocket API service generated from MySQL.",
  "base_url": "https://api.example.com/",
  "token": "change_this_token",
  "tokens_table": "_apiql_tokens",
  "expose_system_tables": false,
  "max_limit_per_page": 100,
  "default_per_page": 20,
  "allow_all": false,
  "allowed_actions": {
    "users": ["list", "insert", "update", "delete"]
  },
  "disabled_tables": [],
  "disabled_columns": [
    "password",
    "password_hash",
    "token",
    "secret",
    "key_hash"
  ],
  "permissions": {},
  "debug": false,
  "hostname": "localhost",
  "port": 3306,
  "username": "dbuser",
  "password": "dbpass",
  "database": "dbname",
  "dbdriver": "mysqli",
  "dsn": "",
  "dbprefix": "",
  "db_debug": false,
  "char_set": "utf8mb4",
  "dbcollat": "utf8mb4_unicode_ci",
  "swap_pre": "",
  "encrypt": false,
  "compress": false,
  "stricton": false,
  "save_queries": true
}
```

## Config key behavior

`listen`

Local host and port used by the Go service. Example: `127.0.0.1:8098`.

`base_url`

Public URL used in generated documentation and examples. Keep trailing slash optional; ApiQL normalizes it.

`token`

Fallback or seed token. If `tokens_table` is `null`, this is the fixed API token. If `tokens_table` is set and the table has no valid tokens, ApiQL seeds this token into the token table.

`tokens_table`

Use `null` for one fixed config token. Use a table name such as `_apiql_tokens` to enable database-managed tokens.

`allow_all`

When `true`, all non-system MySQL tables and views are exposed with list, insert, update and delete actions. `disabled_tables` and `disabled_columns` are ignored in allow-all mode.

`allowed_actions`

Used when `allow_all` is `false`. Defines which tables are endpoints and which CRUD actions are allowed.

Supported actions:

```text
list
insert
update
delete
```

`disabled_tables`

Tables that must not be exposed when `allow_all` is `false`.

`disabled_columns`

Columns removed from reads and blocked from writes when `allow_all` is `false`.

`permissions`

Optional row-level and data-level rules based on token claims.

## Token modes

Fixed token mode:

```json
{
  "token": "change_this_token",
  "tokens_table": null
}
```

In fixed token mode every accepted request authenticates as:

```json
{
  "user_id": 0,
  "role": "admin"
}
```

Token table mode:

```json
{
  "token": "first_admin_token",
  "tokens_table": "_apiql_tokens"
}
```

ApiQL automatically creates the table if it does not exist.

Token table columns:

```text
id              BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
token           VARCHAR(255) UNIQUE
user_id         BIGINT NULL DEFAULT 0
role            VARCHAR(64) DEFAULT 'user'
status          ENUM('valid','invalid') DEFAULT 'valid'
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
last_used_at    DATETIME NULL
```

Only rows with `status = 'valid'` authenticate. `last_used_at` is updated after successful auth.

Insert a new token manually:

```sql
INSERT INTO `_apiql_tokens`
  (`token`, `user_id`, `role`, `status`, `created_at`)
VALUES
  ('user_token_here', 15, 'user', 'valid', NOW());
```

Invalidate a token:

```sql
UPDATE `_apiql_tokens`
SET `status` = 'invalid'
WHERE `token` = 'user_token_here';
```

## Permissions

Use permissions when a token should only read or mutate rows that belong to the authenticated user.

```json
{
  "tokens_table": "_apiql_tokens",
  "allow_all": false,
  "allowed_actions": {
    "users": ["list", "update"],
    "shopping_cart": ["list", "insert", "update", "delete"],
    "messages": ["list", "insert", "update", "delete"]
  },
  "permissions": {
    "users": {
      "read": {
        "id": "{CURRENT_USER_ID}"
      },
      "update": {
        "id": "{CURRENT_USER_ID}"
      }
    },
    "shopping_cart": {
      "read": {
        "user_id": "{CURRENT_USER_ID}"
      },
      "insert_values": {
        "user_id": "{CURRENT_USER_ID}"
      },
      "update": {
        "user_id": "{CURRENT_USER_ID}"
      },
      "update_values": {
        "user_id": "{CURRENT_USER_ID}"
      },
      "delete": {
        "user_id": "{CURRENT_USER_ID}"
      }
    },
    "messages": {
      "read": {
        "receiver_id": "{CURRENT_USER_ID}"
      },
      "insert_values": {
        "sender_id": "{CURRENT_USER_ID}"
      },
      "update": {
        "sender_id": "{CURRENT_USER_ID}"
      },
      "delete": {
        "sender_id": "{CURRENT_USER_ID}"
      }
    }
  }
}
```

Permission rules:

- `read` adds automatic filters to list/get requests.
- `insert_values` forces values during insert. Do not trust the browser for ownership columns.
- `update` allows update only when the existing row matches the rule.
- `update_values` forces values during update.
- `delete` allows delete only when the existing row matches the rule.

Available placeholders:

```text
{CURRENT_USER_ID}
{USER_ID}
{CURRENT_ROLE}
{ROLE}
{TOKEN}
```

For real `{CURRENT_USER_ID}` and `{CURRENT_ROLE}` values, use `tokens_table`. With a fixed config token, `user_id` is `0`.

## REST authentication

Preferred:

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users?limit=20"
```

Also supported:

```bash
curl -H "X-APIQL-TOKEN: YOUR_TOKEN" \
"https://api.example.com/users?limit=20"
```

Query string token is supported but not recommended for production because URLs may be logged:

```bash
curl "https://api.example.com/users?token=YOUR_TOKEN&limit=20"
```

## REST endpoints

List:

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users?limit=20&offset=0"
```

Get one:

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users/5"
```

Insert:

```bash
curl -X POST "https://api.example.com/users" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"full_name":"Mihajlo","email":"mihajlo@example.com"}'
```

Update:

```bash
curl -X POST "https://api.example.com/users/5" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"full_name":"Mihajlo Siljanoski"}'
```

Delete:

```bash
curl -X DELETE "https://api.example.com/users/5" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

## Query options

REST query string:

```text
field[users.id]=id
field[users.full_name]=fullName
search[users.full_name]=mih
filter[users.status]=active
sort[users.id]=DESC
limit=20
offset=0
merge[profiles.id]=profile_id
add[countries.id]=country_id
```

Example:

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users?field[users.id]=id&field[users.full_name]=fullName&filter[users.status]=active&sort[users.id]=DESC&limit=20"
```

JavaScript SDK query object:

```javascript
const users = await apiql.get('users', {
  fields: {
    'users.id': 'id',
    'users.full_name': 'fullName'
  },
  filter: {
    'users.status': 'active'
  },
  search: {
    'users.full_name': 'mih'
  },
  sort: {
    'users.id': 'DESC'
  },
  limit: 20,
  offset: 0
});
```

## Response format

Successful responses include status metadata and data payload:

```json
{
  "error": false,
  "status_text": "OK",
  "status_code": 200,
  "status_msg": "OK",
  "data": []
}
```

Error responses:

```json
{
  "error": true,
  "status_text": "error",
  "status_code": 401,
  "status_msg": "Authentication failed!"
}
```

Agents should check both the HTTP status code and the JSON `error` field.

## WebSocket protocol

Connect:

```text
wss://api.example.com/ws
```

Authenticate first:

```json
{
  "id": "auth_1",
  "action": "auth",
  "token": "YOUR_TOKEN"
}
```

Subscribe to all events for a table:

```json
{
  "id": "sub_1",
  "action": "subscribe",
  "event": "users"
}
```

Subscribe to a specific action:

```json
{
  "id": "sub_2",
  "action": "subscribe",
  "event": "users.update"
}
```

Subscribe with a filter:

```json
{
  "id": "sub_3",
  "action": "subscribe",
  "event": "orders.update",
  "query": {
    "filter": {
      "orders.user_id": 15
    }
  }
}
```

Read through WebSocket:

```json
{
  "id": "get_1",
  "action": "get",
  "table": "users",
  "query": {
    "filter": {
      "users.status": "active"
    },
    "limit": 20
  }
}
```

Insert through WebSocket:

```json
{
  "id": "insert_1",
  "action": "insert",
  "table": "messages",
  "data": {
    "receiver_id": 15,
    "body": "Hello"
  }
}
```

Update through WebSocket:

```json
{
  "id": "update_1",
  "action": "update",
  "table": "users",
  "record_id": "5",
  "data": {
    "full_name": "Mihajlo Siljanoski"
  }
}
```

Delete through WebSocket:

```json
{
  "id": "delete_1",
  "action": "delete",
  "table": "messages",
  "record_id": "5"
}
```

Realtime event message shape:

```json
{
  "type": "event",
  "event": "users.update",
  "table": "users",
  "action": "update",
  "data": {
    "id": "5",
    "full_name": "Mihajlo Siljanoski"
  }
}
```

Realtime events are emitted only by writes that pass through ApiQL v3. Direct MySQL writes do not emit WebSocket events.

## JavaScript SDK

Global SDK:

```html
<script src="https://apiql.net/v3/sdk/apiql-min.js"></script>
```

Service-local SDK:

```html
<script src="https://api.example.com/sdk/apiql-v3.js"></script>
```

Initialize:

```javascript
const apiql = new ApiQL({
  endpoint: 'https://api.example.com',
  token: 'YOUR_TOKEN',
  transport: 'auto' // auto | rest | ws
});
```

CRUD:

```javascript
const users = await apiql.get('users', { limit: 20 });
const user = await apiql.get('users', 5);

await apiql.insert('users', {
  full_name: 'Mihajlo',
  email: 'mihajlo@example.com'
});

await apiql.update('users', 5, {
  full_name: 'Mihajlo Siljanoski'
});

await apiql.delete('users', 5);
```

Realtime:

```javascript
apiql.on('users', function(event){
  console.log(event.action, event.data);
});

apiql.on('users.insert', function(user){
  console.log(user);
});

const listener = apiql.on('users.update', function(user){
  console.log(user);
});

listener.remove();
```

Connection events:

```javascript
apiql.on('connect', function(){
  console.log('Connected');
});

apiql.on('disconnect', function(){
  console.log('Disconnected');
});

apiql.on('reconnect', function(){
  console.log('Reconnected');
});

apiql.on('error', function(error){
  console.error(error);
});
```

The SDK automatically reconnects after WebSocket disconnects and restores subscriptions.

## PHP HTTP example

```php
<?php
$ch = curl_init('https://api.example.com/users?limit=20');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer YOUR_TOKEN',
    ],
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
```

## Apache reverse proxy rules

When manually configuring Apache, both HTTP and WebSocket proxying are required.

```apache
ProxyPreserveHost On
ProxyRequests Off
AllowEncodedSlashes NoDecode
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"

ProxyPass /ws ws://127.0.0.1:8098/ws retry=0 timeout=60
ProxyPassReverse /ws ws://127.0.0.1:8098/ws
ProxyPass / http://127.0.0.1:8098/ retry=0 timeout=60
ProxyPassReverse / http://127.0.0.1:8098/
```

Required Apache modules:

```bash
sudo a2enmod proxy proxy_http proxy_wstunnel headers ssl rewrite
sudo systemctl reload apache2
```

## Validation checklist for agents

After install or config changes, run:

```bash
sudo systemctl restart apiql-v3
sudo systemctl status apiql-v3 --no-pager
curl -fsSL http://127.0.0.1:8098/_health
```

Then test one authenticated request:

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://127.0.0.1:8098/users?limit=1"
```

If a public domain is configured:

```bash
curl -fsSL https://api.example.com/_health
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users?limit=1"
```

Open generated docs:

```text
https://api.example.com/_documentation
```

## Troubleshooting

Service fails immediately:

```bash
sudo journalctl -u apiql-v3 -n 100 --no-pager
```

Common causes:

- config JSON syntax error.
- service user cannot read `/etc/apiql-v3/config.json`.
- MySQL credentials are wrong.
- MySQL host or port is unreachable.
- selected listen port is already used.
- token table cannot be created because DB user lacks `CREATE TABLE`.

Fix config permissions:

```bash
sudo chown root:apiql /etc/apiql-v3
sudo chmod 0750 /etc/apiql-v3
sudo chown root:apiql /etc/apiql-v3/config.json
sudo chmod 0640 /etc/apiql-v3/config.json
sudo systemctl restart apiql-v3
```

Check which process uses a port:

```bash
sudo ss -ltnp | grep ':8098'
```

Change port:

```bash
sudo nano /etc/apiql-v3/config.json
sudo systemctl restart apiql-v3
```

Set `"listen"` to a free address such as:

```json
{
  "listen": "127.0.0.1:8105"
}
```

Authentication fails:

- Check the request header is exactly `Authorization: Bearer YOUR_TOKEN`.
- If using `tokens_table`, verify the token row has `status = 'valid'`.
- Verify the service restarted after config changes.
- Do not test with a token copied from logs if it may have whitespace.

Endpoint not found:

- If `allow_all` is `false`, add the table to `allowed_actions`.
- If the table is in `disabled_tables`, remove it.
- Check that the table exists in the configured MySQL database.
- Restart the service and refresh `/_documentation`.

No realtime events:

- Confirm the browser is connected to `/ws`.
- Confirm auth succeeded over WebSocket.
- Subscribe to the correct event name, for example `users` or `users.update`.
- Remember that direct MySQL writes do not emit events. Writes must pass through ApiQL v3.

## Safe automation notes

For unattended automation:

- Use environment variables for installer input.
- Do not write secrets to shell history where possible.
- Prefer token table mode and create a dedicated token per integration.
- Use least-privilege MySQL credentials where the deployment allows it.
- Keep ApiQL bound to `127.0.0.1` behind Apache/Nginx unless the user explicitly wants direct network exposure.
- Verify public TLS before using `wss://`.

## Quick agent prompt

Use this prompt when delegating work to another AI agent:

```text
You are working with ApiQL v3. Read https://apiql.net/documentation/v3/downloads/AGENTS.md first.
Install with the official install.sh, configure /etc/apiql-v3/config.json, restart apiql-v3 after config changes, verify /_health and /_documentation, and use REST/WebSocket exactly as documented. Do not expose tokens or database passwords.
```
