Hosting & administration
Configure a SQL database
Connect an OPEN//77 server to MySQL or MariaDB: install SQL, create a dedicated account, configure credentials, verify readiness from Lua, diagnose failures and back up persistent data.
On this page8 sections
Configure the built-in asynchronous MySQL/MariaDB bridge for server Lua. MySQL and Open77.database expose the same API; no oxmysql resource is required. PostgreSQL, SQLite and Microsoft SQL Server are not supported.
SQL runs on the server side, never on a player's PC. The bridge is disabled by default. The server distribution does not install a database service or create SQL credentials for you. For a small local key/value store without a SQL service, consider server KVP instead.
1. Install MySQL or MariaDB
You can use an existing compatible service. For a new Debian/Ubuntu host, install MariaDB from the configured distribution repositories:
sudo apt update
sudo apt install mariadb-server mariadb-client
sudo systemctl enable --now mariadb
sudo systemctl status mariadb
sudo mariadb-secure-installation
On Windows, use the MariaDB installer, enable the database as a Windows
service and keep its TCP port at 3306 unless another service already uses
it. For other distributions or installer details, follow the
official MariaDB installation guide.
The simplest arrangement is SQL and OPEN//77 on the same host, with SQL listening on loopback. Players connect to the game server, not to port 3306. Do not open that port to the public Internet for a local database.
2. Create a database and a dedicated account
Open an administrative SQL session. On a typical Debian/Ubuntu MariaDB
installation, sudo mariadb uses the local administrative socket. On Windows,
use the installed client or a database manager with your administrator account.
Run this once, replacing the password placeholder with a unique secret:
CREATE DATABASE open77 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'open77'@'127.0.0.1' IDENTIFIED BY 'REPLACE_WITH_A_UNIQUE_PASSWORD';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX
ON open77.* TO 'open77'@'127.0.0.1';
These grants allow normal reads/writes and common table migrations in this
database only. Review each resource's schema requirements: add privileges
such as DROP only when its reviewed migration actually requires them. Do not
give the game server the SQL administrator account or global *.* privileges.
SQL accounts include a host component. An account at 127.0.0.1 is not a
catch-all account for remote servers; MariaDB distinguishes TCP loopback from
localhost socket authentication. Keep the account host and connection method
consistent. See MariaDB's account rules.
Test the same TCP connection that OPEN//77 will use, from the game server's machine:
mariadb --protocol=TCP --host=127.0.0.1 --port=3306 --user=open77 --password open77
The client prompts for the password; do not put it directly in the command.
The equivalent MySQL client command starts with mysql. In that session:
SELECT DATABASE(), CURRENT_USER(), 1 AS connection_ok;
This proves authentication and database selection, not every resource's migration permissions.
3. Configure OPEN//77
Merge this object into the existing server.jsonc you actually launch;
do not replace the rest of the file:
{
"database": {
"enabled": true,
"connectionString": "",
"connectionStringEnvironmentVariable": "OP77_DATABASE_CONNECTION",
"maxRows": 10000
}
}
| Setting | Meaning |
|---|---|
database.enabled |
false by default. Must be true to create the bridge. |
database.connectionString |
An explicit nonblank value takes precedence over the environment. Leave it empty when using an environment secret. |
database.connectionStringEnvironmentVariable |
Name of the variable to read; defaults to OP77_DATABASE_CONNECTION. This field is a variable name, not the password. |
database.maxRows |
Maximum rows returned by one query: default 10000, accepted range 1..1000000. Extra rows are truncated with a warning, not paginated automatically. |
Supply a MySqlConnector connection string
to the dedicated-server process. This is a semicolon-separated string, not
a mysql:// URL or FiveM's set mysql_connection_string directive.
Linux, for a manual development launch from this shell:
export OP77_DATABASE_CONNECTION='Server=127.0.0.1;Port=3306;Database=open77;User ID=open77;Password=REPLACE_WITH_A_UNIQUE_PASSWORD;'
# Launch your normal OPEN//77 server command from this same shell.
Windows PowerShell, for a manual development launch:
$env:OP77_DATABASE_CONNECTION = 'Server=127.0.0.1;Port=3306;Database=open77;User ID=open77;Password=REPLACE_WITH_A_UNIQUE_PASSWORD;'
# Launch your normal OPEN//77 server command from this same PowerShell session.
These examples show syntax with a placeholder. Entering a real secret directly
can leave it in shell history; use your host's protected secret/configuration
mechanism for production. For systemd, containers or hosting panels, configure
the environment in that service/container/panel instead: exporting it in an
unrelated SSH shell does not change a running service's environment. Restrict
access to any environment file containing credentials. OPEN//77 does not
implicitly load an arbitrary .env file.
Restart the dedicated server after changing these settings. Reloading only a Lua resource does not recreate the server-wide database bridge. An enabled bridge with no effective connection string fails configuration validation.
Remote SQL and containers
For a separate SQL host, use its reachable hostname in Server, create a SQL
account restricted to the game server's connecting address and restrict the
database firewall to that source. In a container, 127.0.0.1 means that
container; another container normally needs its service/DNS name and a shared
network. Use a persistent volume for the SQL data directory.
For remote production connections, use TLS with certificate and hostname validation:
Server=db.example.net;Port=3306;Database=open77;User ID=open77;Password=REPLACE_WITH_A_UNIQUE_PASSWORD;SslMode=VerifyFull;
If your provider uses a private CA, add its supplied SslCa file path. The
hostname must match the certificate. SslMode=Required encrypts but does not
validate the certificate's identity; it is not equivalent to VerifyFull.
See the connector's TLS options.
4. Verify readiness from Lua
Only server scripts with database.access can use SQL. Put these two files
in a resource named sql_probe under your configured resources directory:
-- open77.lua
resource "sql_probe"
version "1.0.0"
server_script "server.lua"
permissions { "database.access" }
-- server.lua
local queued, reason = MySQL.ready(function()
local ok, result = pcall(function()
return MySQL.scalar.await("SELECT ?", { 77 })
end)
if not ok then
print("[sql_probe] query failed: " .. tostring(result))
return
end
print("[sql_probe] connected; parameterized SELECT returned " .. tostring(result))
end)
if not queued then
print("[sql_probe] readiness refused: " .. tostring(reason))
else
local ready, state = MySQL.isReady()
print("[sql_probe] state: " .. (ready and "ready" or tostring(state)))
end
From the server console or Warden console:
refresh
ensure sql_probe
Expected server messages include:
Database bridge enabled (maxRows=10000).
Database bridge reachable; MySQL.ready handlers released.
[sql_probe] connected; parameterized SELECT returned 77
The first message alone only means the bridge is configured. The second is
logged after a real SELECT 1 probe succeeds. The Lua check additionally
verifies the resource's permission, scheduler and parameter binding without
creating or changing any tables.
MySQL.ready is a startup gate, not a continuous health check. Before the
first successful connection, the host retries with a backoff up to 30 seconds.
After that first success, handle failures of individual queries; readiness
does not revert to false during a later outage.
5. Use SQL in resources
Callback methods receive (result, error). The .await variants must run in
a host-managed coroutine, such as CreateThread, an event handler or a
MySQL.ready callback. Queries run off the server tick; their continuations
resume on the owning resource's scheduler.
MySQL.ready(function()
local ok, reason = pcall(function()
MySQL.update.await([[
CREATE TABLE IF NOT EXISTS my_resource_settings (
setting_key VARCHAR(64) PRIMARY KEY,
setting_value VARCHAR(255) NOT NULL
)
]])
MySQL.update.await([[
INSERT INTO my_resource_settings (setting_key, setting_value)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)
]], { "welcome", "Welcome to Night City" })
end)
if not ok then print("[my_resource] database setup failed: " .. tostring(reason)) end
end)
Bind values with ? or named @parameters; never concatenate player input
into SQL. Use transactions for changes that must succeed together and handle
rollback results. Prefix tables with the resource name and key player data by
a persistent authenticated identity, not a temporary player/session ID.
All resources with database.access share this server's configured database
and SQL account. The permission does not create a private schema for each
resource. Enabling SQL also activates persistence paths in other loaded
resources that requested it, such as appearance storage. Review those resources
and back up the database before changing a live server's persistence behavior.
See the database API reference for method shapes, readiness and transactions, and the resource isolation explanation.
Troubleshooting
| Symptom | What to check |
|---|---|
| Startup rejects an enabled database configuration | The effective connection string is empty, the environment variable name is blank or maxRows is outside its allowed range. |
Database bridge disabled. / database_unavailable |
Check the launched configuration path and database.enabled; restart the server after changes. MySQL.ready will not run when disabled. |
database_connecting |
The initial probe is pending. Wait for the reachable message. |
database_unreachable / configured but not answering |
Read the first probe warning; test the same host, port and account from the server machine. Check the SQL service, network and credentials. |
| Access denied | Check password, account host, authentication method and grants. A local administrator's socket login does not prove the application TCP login works. |
| Unknown database | Create the named database and check its exact spelling; the bridge does not create it. |
| Connection refused or timeout | Check the SQL listener, TCP port, container network, firewall and host name. Do not open SQL to every IP as a workaround. |
| TLS/certificate failure | Use the certificate's hostname, the correct CA and a valid server certificate. Check the host clock. Do not disable verification to hide a production configuration error. |
permission_denied:database.access |
Declare the permission in the calling resource and reload that resource. |
MySQL is missing in Lua |
The code is running on the client, or on a server build without this API; keep database code in server_script. |
| Missing table or migration denied | Run the resource's reviewed migration and grant only its required privileges on this database. |
| Queries fail after the ready callback ran | Treat each query failure explicitly. Startup readiness is latched, not a live availability guarantee. |
| Fewer rows than expected | Check the maxRows warning; use explicit ordering and bounded/paginated queries instead of relying on truncation. |
Before sharing logs, remove credentials, connection strings and sensitive
query data. Keep regular database backups and test restoration to a separate
database. A copy of resources/ is not a backup of SQL data.
Implementation references
The operator settings and lifecycle above are defined by
DatabaseConfiguration and ResolveDatabaseConnectionString in the server's
configuration loader, DatabaseService for the real readiness probe, and the
platform database guide.
These are independent of the newer client WebUI changes; configuring SQL
does not require a browser UI or a client-side database password.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
