One Service. REST and Realtime.
ApiQL v3 is a standalone Go binary that turns MySQL tables and views into REST endpoints and WebSocket realtime events using one shared auth, permission, query and CRUD engine.
Read and write records with normal HTTP requests.
Subscribe to table events generated by ApiQL CRUD writes.
Browser
-> ApiQL JS SDK
-> REST / WebSocket
-> ApiQL Engine
-> MySQL
What changed in v3
v3 lives as its own compiled Go service and does not depend on the v2 runtime.
Insert, update and delete operations publish events to matching WebSocket subscriptions.
get, insert, update, delete, on and off.
Installation
The installer downloads the latest compiled Linux binary, creates an external JSON config, installs a systemd service and can optionally prepare an Apache reverse proxy.
Basic install
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo bash
Edit config
sudo nano /etc/apiql-v3/config.json
Start or restart service
sudo systemctl enable --now apiql-v3
sudo systemctl restart apiql-v3
Install with database values
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
By default the installer starts from 127.0.0.1:8098. If that port is already used by another service, it automatically selects the next free port and writes it to /etc/apiql-v3/config.json.
Install on a custom port
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
For a full listen address, use APIQL_LISTEN instead:
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 domain and WebSocket proxy
curl -fsSL https://apiql.net/documentation/v3/downloads/install.sh | sudo env \
APIQL_DOMAIN=api.example.com \
APIQL_EMAIL=admin_at_example_dot_com \
APIQL_DB_NAME=my_database \
APIQL_DB_USER=my_user \
APIQL_DB_PASS='my_password' \
APIQL_TOKEN='change_this_token' \
bash
Replace admin_at_example_dot_com with your real certificate email address before running the command.
Configuration
ApiQL v3 keeps configuration outside the binary. Users receive the compiled binary and edit only the JSON file.
{
"listen": "127.0.0.1:8098",
"app_name": "ApiQL v3 project",
"base_url": "https://api.example.com/",
"token": "change_this_token",
"tokens_table": "_apiql_tokens",
"max_limit_per_page": 100,
"default_per_page": 20,
"allow_all": false,
"allowed_actions": {
"users": ["list", "insert", "update", "delete"]
},
"disabled_columns": ["password", "token", "secret"],
"hostname": "localhost",
"port": 3306,
"username": "dbuser",
"password": "dbpass",
"database": "dbname"
}
| Key | Meaning |
|---|---|
listen | Local address where the Go service listens. Default v3 port is 8098. |
base_url | Public URL used in generated runtime documentation and SDK examples. |
allow_all | When true, every non-system MySQL table/view becomes an endpoint with all actions enabled. |
allowed_actions | Table-by-table action list when allow_all is false. |
disabled_columns | Columns removed from reads and blocked from writes unless allow_all is enabled. |
permissions | Optional row-level read filters and automatic insert/update values based on token claims. |
Token authentication
REST accepts Authorization: Bearer TOKEN, X-APIQL-TOKEN: TOKEN or ?token=TOKEN. WebSocket clients connect first, then send an auth message.
{
"token": "change_this_token",
"tokens_table": null
}
ApiQL compares every request token with the fixed JSON token. The authenticated role is admin and user_id is 0.
{
"token": "first_admin_token",
"tokens_table": "_apiql_tokens"
}
ApiQL creates the table automatically and validates rows where status = "valid".
Automatic token table schema
| Column | Purpose |
|---|---|
id | Token row id. |
token | Unique API token. |
user_id | User claim available as {CURRENT_USER_ID}. |
role | Role claim available as {CURRENT_ROLE}. |
status | valid or invalid. |
created_at | Creation timestamp. |
last_used_at | Updated automatically on successful auth. |
Service commands
sudo systemctl status apiql-v3
sudo systemctl restart apiql-v3
sudo journalctl -u apiql-v3 -f
Runtime docs from the service are available at:
http://127.0.0.1:8098/_documentation
https://api.example.com/_documentation
REST transport
Get records
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users?limit=20&offset=0"
Get one record
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.example.com/users/5"
Insert
curl -X POST "https://api.example.com/users" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"full_name":"Mihajlo","email":"mihajlo_at_example.com"}'
Update
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
curl -X DELETE "https://api.example.com/users/5" \
-H "Authorization: Bearer YOUR_TOKEN"
WebSocket transport
Connect to wss://api.example.com/ws, authenticate, then send CRUD or subscribe messages.
Authenticate
{"action":"auth","token":"YOUR_TOKEN"}
Subscribe
{"action":"subscribe","event":"users.update"}
Read through WebSocket
{
"id": "req_1",
"action": "get",
"table": "users",
"query": {
"filter": { "users.status": "active" },
"limit": 20
}
}
Event message
{
"type": "event",
"event": "users.update",
"table": "users",
"action": "update",
"data": { "id": "5", "full_name": "Mihajlo" }
}
JavaScript SDK
The SDK chooses WebSocket automatically when possible and falls back to REST for CRUD requests in transport: "auto".
Use the SDK from your own v3 service, or use the global minified SDK link from ApiQL.net.
Service-local SDK
<script src="https://api.example.com/sdk/apiql-v3.js"></script>
<script>
const apiql = new ApiQL({
endpoint: 'https://api.example.com',
token: 'YOUR_TOKEN',
transport: 'auto' // auto | rest | ws
});
</script>
Global SDK link
<script src="https://apiql.net/v3/sdk/apiql-min.js"></script>
<script>
const apiql = new ApiQL({
endpoint: 'https://api.example.com',
token: 'YOUR_TOKEN',
transport: 'auto' // auto | rest | ws
});
</script>
Connection events
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);
});
CRUD
const users = await apiql.get('users');
const user = await apiql.get('users', 5);
await apiql.insert('users', {
full_name: 'Mihajlo',
email: 'mihajlo_at_example.com'
});
await apiql.update('users', 5, {
full_name: 'Mihajlo Siljanoski'
});
await apiql.delete('users', 5);
Realtime
apiql.on('users', function(event){
console.log(event.action);
console.log(event.data);
});
apiql.on('users.insert', function(user){
console.log(user);
});
const listener = apiql.on('users.update', function(user){
console.log(user);
});
listener.remove();
Realistic JavaScript SDK examples
These examples use practical table names and workflows. Adjust table and column names to match your MySQL schema and keep sensitive columns disabled in the JSON config.
Users admin screen
Load users with fields, search, filters and realtime refresh after any user insert, update or delete.
const apiql = new ApiQL({
endpoint: 'https://api.example.com',
token: sessionStorage.getItem('api_token'),
transport: 'auto'
});
const usersState = {
search: '',
status: 'active'
};
async function loadUsers(){
const users = await apiql.get('users', {
fields: {
'users.id': 'id',
'users.full_name': 'fullName',
'users.email': 'email',
'users.status': 'status',
'users.created_at': 'createdAt'
},
search: usersState.search ? {
'users.full_name': usersState.search
} : {},
filter: usersState.status ? {
'users.status': usersState.status
} : {},
sort: {
'users.id': 'DESC'
},
limit: 25,
offset: 0
});
renderUsersTable(users);
}
async function createUser(form){
await apiql.insert('users', {
full_name: form.fullName.value,
email: form.email.value,
status: 'active'
});
await loadUsers();
}
async function disableUser(userId){
await apiql.update('users', userId, {
status: 'disabled'
});
}
document.querySelector('#user-search').addEventListener('input', function(){
usersState.search = this.value.trim();
loadUsers();
});
await loadUsers();
apiql.on('users', loadUsers);
Shopping cart
Keep a cart synchronized across browser tabs and devices. Every insert, update or delete in cart_items refreshes the visible cart.
const cartId = getCurrentCartId();
async function loadCart(){
const items = await apiql.get('cart_items', {
fields: {
'cart_items.id': 'id',
'cart_items.product_id': 'productId',
'cart_items.quantity': 'quantity',
'cart_items.price': 'price'
},
filter: {
'cart_items.cart_id': cartId
},
add: {
'products.id': 'product_id'
},
sort: {
'cart_items.id': 'ASC'
}
});
renderCart(items);
renderCartTotal(items.reduce(function(total, item){
return total + Number(item.price) * Number(item.quantity);
}, 0));
}
async function addToCart(productId, quantity){
await apiql.insert('cart_items', {
cart_id: cartId,
product_id: productId,
quantity: quantity || 1
});
}
async function changeQuantity(itemId, quantity){
if (quantity <= 0) {
await apiql.delete('cart_items', itemId);
return;
}
await apiql.update('cart_items', itemId, {
quantity: quantity
});
}
await loadCart();
apiql.on('cart_items', loadCart, {
filter: {
'cart_items.cart_id': cartId
}
});
Order status page
Show a customer live order progress without polling. When the backend updates the order through ApiQL, the browser receives the change.
const orderId = getOrderIdFromUrl();
async function loadOrder(){
const order = await apiql.get('orders', orderId);
renderOrderStatus(order.status);
renderOrderSummary(order);
}
await loadOrder();
apiql.on('orders.update', function(order){
if (String(order.id) !== String(orderId)) {
return;
}
renderOrderStatus(order.status);
showMessage('Order status updated to ' + order.status);
}, {
filter: {
'orders.id': orderId
}
});
Live notifications
Use token permissions to return only notifications for the authenticated user, then subscribe to new rows.
async function loadNotifications(){
const notifications = await apiql.get('notifications', {
filter: {
'notifications.is_read': 0
},
sort: {
'notifications.id': 'DESC'
},
limit: 15
});
renderNotifications(notifications);
}
await loadNotifications();
apiql.on('notifications.insert', function(notification){
showToast(notification.title, notification.body);
loadNotifications();
});
async function markNotificationRead(notificationId){
await apiql.update('notifications', notificationId, {
is_read: 1
});
await loadNotifications();
}
Realtime dashboard counters
Create a MySQL view such as dashboard_stats, expose it as a read-only endpoint, and refresh it when related tables change.
async function loadDashboardStats(){
const rows = await apiql.get('dashboard_stats', {
limit: 1
});
const stats = rows[0] || {};
document.querySelector('#active-users').textContent = stats.active_users || 0;
document.querySelector('#pending-orders').textContent = stats.pending_orders || 0;
document.querySelector('#open-tickets').textContent = stats.open_tickets || 0;
}
await loadDashboardStats();
apiql.on('users', loadDashboardStats);
apiql.on('orders', loadDashboardStats);
apiql.on('support_tickets', loadDashboardStats);
PHP examples
v3 examples use plain PHP HTTP requests. There is no PHP SDK section for v3.
GET
<?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);
POST
<?php
$payload = json_encode([
'full_name' => 'Mihajlo',
'email' => 'mihajlo_at_example.com',
]);
$ch = curl_init('https://api.example.com/users');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_TOKEN',
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
curl_close($ch);
Query options
| Feature | REST | JS SDK |
|---|---|---|
| Fields | field[users.id]=userId | { fields: { 'users.id': 'userId' } } |
| Search | search[users.full_name]=mih | { search: { 'users.full_name': 'mih' } } |
| Filter | filter[users.status]=active | { filter: { 'users.status': 'active' } } |
| Sort | sort[users.id]=DESC | { sort: { 'users.id': 'DESC' } } |
| Pagination | limit=20&offset=0 | { limit: 20, offset: 0 } |
| Merge/JOIN | merge[profiles.id]=profile_id | { merge: { 'profiles.id': 'profile_id' } } |
| Add related row | add[countries.id]=country_id | { add: { 'countries.id': 'country_id' } } |
Complex SDK query
const users = await apiql.get('users', {
fields: {
'users.id': 'userId',
'users.full_name': 'fullName',
'profiles.avatar': 'avatar'
},
merge: {
'profiles.id': 'profile_id'
},
search: {
'users.full_name': 'mih'
},
filter: {
'users.status': 'active'
},
sort: {
'users.id': 'DESC'
},
limit: 20,
offset: 0
});
Realtime rules
- Realtime events are generated only by ApiQL v3 CRUD operations.
- Direct MySQL changes do not emit events because they do not pass through ApiQL.
- REST writes and WebSocket writes both publish to the same internal event bus.
- Subscriptions support
*, table events likeusers, and action events likeusers.update. - When WebSocket reconnects, the JS SDK reauthenticates and restores subscriptions automatically.
Security and SQL safety
- All table and column identifiers must match
[A-Za-z0-9_]. - Generated SQL wraps identifiers in backticks, so MySQL reserved words such as
cascadeare handled as`cascade`. - Values are passed as SQL parameters, not string-concatenated.
- The configured token table is treated as an internal system table unless
expose_system_tablesis explicitly enabled. - Use
allow_all: trueonly for trusted internal systems.