GopServer
Universal Multi-Database REST API Server
Configure once, expose any database table as a fully-featured REST API — no code required, no recompilation needed.
1. What is GopServer?
GopServer is a universal REST API server that supports multiple relational database engines. Once configured, it automatically exposes full CRUD operations (Create, Read, Update, Delete) for any table in your database — without writing any extra code and without recompiling the application.
Key Features
- Full CRUD for any table, controlled via a JSON configuration file
- Switch database engine: one parameter in
app.conf+ restart - Authentication via JWT (Bearer token) or session cookie
- Static API token (no expiry) for server-to-server integrations
- Advanced filtering, sorting and pagination on any column
- CORS support for web frontends
- Multi-language: all API messages configured from a text file
- Add a new table: edit a JSON file + restart — no recompilation
- Built-in brute-force protection on login endpoints
- Environment variable support for sensitive credentials
2. System Requirements
- Operating system: Windows, Linux or macOS (64-bit)
- Port 8080 available (configurable in
app.conf) - No runtime installation required — the executable is statically compiled
- At least one of the following database servers:
| Engine | Minimum Version | Default Port | Notes |
|---|---|---|---|
| PostgreSQL | 12+ | 5432 | |
| MySQL / MariaDB | 5.7+ / 10.3+ | 3306 | |
| SQLite | 3.x | — | File-based, no server needed |
| Microsoft SQL Server | 2012+ | 1433 | |
| Oracle Database | 12c Release 2+ | 1521 | db_name = service name |
Minimum Files on Disk
gopserver.exe (or ./gopserver on Linux/macOS)
conf/
app.conf
tables.json
messages.json
logs/ (created automatically on first start)
3. File Structure
| File | Purpose | Edit without recompile? |
|---|---|---|
gopserver.exe | Server executable | — |
conf/app.conf | Main configuration (DB, JWT, auth, ports) | Yes (restart required) |
conf/tables.json | Table definitions exposed via API | Yes (restart required) |
conf/messages.json | API messages and translations | Yes (restart required) |
logs/app.log | Application log file | Read-only |
4. app.conf Reference
The file conf/app.conf controls all server behavior. It has two environment sections: [dev] for development and [prod] for production. The active section is chosen by the value runmode = dev or runmode = prod.
General Settings
appname = gopserver # Application name (informational) runmode = dev # Active mode: dev or prod httpport = 8080 # Port the server listens on language = EN # API message language: EN, RO, or any code in messages.json # Required to read POST/PUT request body — do not change copyrequestbody = true
Session Settings (for cookie-based web clients)
sessionon = true sessionname = gopserver_session sessiongcmaxlifetime = 3600 # Session duration in seconds (1 hour)
Logging
log_level = 7 # dev: 7 (Debug = all messages) # prod: 3 (Error = errors only) # Levels: 0=Emergency 1=Alert 2=Critical 3=Error # 4=Warning 5=Notice 6=Info 7=Debug
External Configuration Files
tables_file = conf/tables.json # Path to table definitions messages_file = conf/messages.json # Path to API messages
Database Settings
db_type = postgres # postgres | mysql | sqlite | mssql | oracle db_server = localhost # Server address (IP or hostname) db_port = 5432 # Server port db_user = app_user # Database username db_password = your_password # or: ${DB_PASSWORD} db_name = your_db # Database name (SQLite: file path) db_max_open_conns = 25 # Max simultaneous connections db_max_idle_conns = 5 # Idle connections kept in pool db_conn_max_lifetime_minutes = 5 # Max connection lifetime (minutes)
| db_type value | Server | Default Port | Notes |
|---|---|---|---|
postgres | PostgreSQL | 5432 | |
mysql | MySQL / MariaDB | 3306 | |
sqlite | SQLite (local file) | — | db_name = file path |
mssql | Microsoft SQL Server | 1433 | |
oracle | Oracle Database | 1521 | db_name = service name |
db_server, db_port, db_user, db_password are ignored. Set db_name to the file path, e.g. ./data.db. The file is created automatically if it does not exist.
db_name must be the Oracle service name (not SID). Example: db_name = XEPDB1
Authentication Settings
This is not your application's full user management system. Your frontend application is free to use a completely separate table for end-user login, roles, permissions, and access control. GopServer simply needs one table to authenticate API consumers (the users or services that are allowed to call the API).
auth_table = users # Table used for login auth_columns = email;phone;username # Identifier columns in priority order (separated by ;) # The client can send any of them auth_pass_column = password # Column with the hashed password auth_salt_column = salt # Column with the salt (leave empty if not using salt)
Example with a single identifier:
auth_columns = username
Example without salt (password is plain SHA-512):
auth_salt_column =
JWT & Token Settings
jwt_secret = your-random-secret-here # REQUIRED: use a random key of at least 32 characters # Generate on Linux/macOS: openssl rand -hex 32 # Can use env var: jwt_secret = ${JWT_SECRET} api_token = # Optional static token — never expires # For scripts, external services, server-to-server integrations # Send as: Authorization: Bearer your_token_value # Leave empty if not needed
Raw SQL Queries
allow_raw_query = false # Enables GET /api/v1/raw?q=... # Allows arbitrary SELECT queries: JOINs, # subqueries, IN(), GROUP BY, aggregates etc. # false = endpoint disabled (default — safe) # true = enabled (recommended only in dev or # if all authenticated users can see all data)
License Key
license_key = your-license-key-here # License key received from the vendor after submitting your HWID # Without a valid key: GET works (max 3 records), POST/PUT/DELETE disabled # Leave empty if you don't have a key yet: license_key =
Rate Limiting (Brute-Force Protection)
auth_max_attempts = 5 # Consecutive failed attempts per IP before lockout # 0 = rate limiting disabled auth_lockout_seconds = 300 # Lockout duration in seconds # dev: 60 (1 minute), prod: 300 (5 minutes)
When a client exceeds the limit, the server responds with HTTP 429:
{ "success": false, "error": "Too many failed attempts. Try again in 287 seconds." }
X-Forwarded-For or X-Real-IP headers.
Environment Variables
Any value in app.conf can be replaced with an environment variable using the ${VAR_NAME} syntax. The server reads the variable at startup; if the variable is missing or empty, the value from app.conf is used as fallback.
db_password = ${DB_PASSWORD} db_user = ${DB_USER} jwt_secret = ${JWT_SECRET} api_token = ${API_TOKEN}
Setting variables — Windows (Command Prompt)
set DB_PASSWORD=my_strong_password gopserver.exe
Setting variables — Windows (PowerShell)
$env:DB_PASSWORD = "my_strong_password" .\gopserver.exe
Setting variables — Linux / macOS
export DB_PASSWORD="my_strong_password" ./gopserver
app.conf free of credentials. The file can then be safely version-controlled (git) without exposing passwords.
Complete app.conf Example
appname = gopserver runmode = prod httpport = 8080 language = EN copyrequestbody = true sessionon = true sessionname = gopserver_session sessiongcmaxlifetime = 3600 auth_table = users auth_columns = email;phone auth_pass_column = password auth_salt_column = salt log_level = 7 tables_file = conf/tables.json messages_file = conf/messages.json allow_raw_query = false [dev] httpport = 8080 db_type = postgres db_server = localhost db_port = 5432 db_user = dev_user db_password = dev_password db_name = mydb_dev log_level = 7 jwt_secret = dev-secret-change-in-prod allow_raw_query = true auth_max_attempts = 5 auth_lockout_seconds = 60 [prod] httpport = 8080 db_type = postgres db_server = 192.168.1.100 db_port = 5432 db_user = ${DB_USER} db_password = ${DB_PASSWORD} db_name = mydb_production log_level = 3 jwt_secret = ${JWT_SECRET} allow_raw_query = false auth_max_attempts = 5 auth_lockout_seconds = 300
5. tables.json Reference
This file tells the server which tables are accessible via the API and how they behave.
Format
{
"table_name": {
"pk": "primary_key_column",
"columns": ["col1", "col2", "col3"],
"hidden": ["secret_col1"],
"locked": ["immutable_col1"],
"allow_post": 1,
"allow_put": 1,
"allow_delete": 0
}
}
| Field | Description | Default |
|---|---|---|
pk | Primary key column — used for GET/PUT/DELETE by ID | id_ |
columns | All columns to expose via API. Order determines response field order. | Required |
hidden | Excluded from GET responses AND cannot be set via POST (e.g. password, salt) | [] |
locked | Cannot be modified via PUT once created (e.g. created_at, unique_code) | [] |
allow_post | 1 = allow record creation (POST); 0 = blocked | 0 |
allow_put | 1 = allow record update (PUT); 0 = blocked | 0 |
allow_delete | 1 = allow record deletion (DELETE); 0 = blocked | 0 |
tables.json, GET (read) is always available. allow_post, allow_put, allow_delete control write operations only.
Field Access Matrix
| Field Type | GET | POST | PUT |
|---|---|---|---|
| normal | YES | YES | YES |
| hidden | NO | NO | YES |
| locked | YES | YES | NO |
| hidden + locked | NO | NO | NO |
Common pattern: Put password and salt in both hidden and locked to make them completely inaccessible via API.
Rules
- Columns in
hiddenmust also appear incolumns - Columns in
lockedmust also appear incolumns - The
pkcolumn must appear incolumns - Column names must exactly match the database column names (case-sensitive, usually lowercase)
- Any column absent from
columnsis completely inaccessible via API
Example
{
"users": {
"pk": "id",
"columns": ["id", "name", "email", "phone", "password", "salt",
"active", "role", "created_at"],
"hidden": ["password", "salt"],
"locked": ["password", "salt", "created_at"],
"allow_post": 1,
"allow_put": 1,
"allow_delete": 0
},
"products": {
"pk": "id",
"columns": ["id", "code", "name", "category", "price", "stock", "unit"],
"hidden": [],
"locked": [],
"allow_post": 1,
"allow_put": 1,
"allow_delete": 1
},
"orders": {
"pk": "order_id",
"columns": ["order_id", "client_id", "order_date", "total", "status"],
"hidden": [],
"locked": ["client_id", "order_date"],
"allow_post": 1,
"allow_put": 1,
"allow_delete": 0
}
}
How to Add a New Table
- Make sure the table exists in the database
- Open
conf/tables.json - Add a new block (remember the comma after the previous block)
- Save the file
- Restart the server
Done — the new table is immediately accessible at /api/v1/table_name.
6. messages.json Reference
All messages returned by the API and written to log files are configurable in this file. You can modify texts or add new languages without recompiling.
Format
{
"EN": {
"message_key": "The message text",
"message_with_param": "An error occurred: %s"
},
"RO": {
"message_key": "Textul mesajului",
"message_with_param": "A aparut eroarea: %s"
}
}
| Placeholder | Type |
|---|---|
%s | String |
%d | Integer |
%v | Any type (used in technical log errors) |
%f | Decimal number |
How to Add a New Language (e.g. French)
- Open
conf/messages.json - Add a new block after the last one:
, "FR": { "internal_error": "Erreur interne du serveur", "record_not_found": "Enregistrement non trouvé" // ... all keys, or only the ones you want to override }
- In
conf/app.confset:language = FR - Restart the server
7. Authentication
The server supports 3 authentication methods, checked in this order:
- Static token (
api_tokeninapp.conf) — for external services - JWT Bearer token — for mobile apps and SPAs
- Beego session cookie — for classic web applications
All routes under /api/v1/* are protected. The /health route is public.
Password Storage
Passwords must be stored as SHA-512 of the concatenation of password + salt:
hash = SHA512(password + salt)
PostgreSQL (with pgcrypto extension)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users (name, email, password, salt)
VALUES (
'John Smith',
'john@example.com',
encode(digest('mypassword123' || 'random_salt_value', 'sha512'), 'hex'),
'random_salt_value'
);
MySQL / MariaDB
INSERT INTO users (name, email, password, salt)
VALUES (
'John Smith',
'john@example.com',
SHA2(CONCAT('mypassword123', 'random_salt_value'), 512),
'random_salt_value'
);
MSSQL
INSERT INTO users (name, email, password, salt)
VALUES (
'John Smith',
'john@example.com',
CONVERT(VARCHAR(128), HASHBYTES('SHA2_512', 'mypassword123' + 'random_salt_value'), 2),
'random_salt_value'
);
SQLite / Oracle
Compute the SHA-512 hash in your application before inserting. Neither SQLite nor Oracle have a standard built-in SHA-512 function accessible directly in SQL.
Without salt (auth_salt_column empty in app.conf):
hash = SHA512(password)
-- PostgreSQL example:
encode(digest('mypassword123', 'sha512'), 'hex')
Method 1: JWT (recommended for mobile / SPA)
Step 1 — Login to get the token:
POST /api/v1/auth/login
Content-Type: application/json
{ "email": "john@example.com", "password": "mypassword123" }
You can use any configured identifier column:
{ "phone": "+1234567890", "password": "mypassword123" }
Success response:
{
"success": true,
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Step 2 — Use the token on every request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Method 2: Session Cookie (for browser-based web apps)
POST /api/v1/auth/login-session
Content-Type: application/json
{ "email": "john@example.com", "password": "mypassword123" }
The browser automatically receives the session cookie (gopserver_session). Subsequent requests send the cookie automatically.
POST /api/v1/auth/logout # Destroys the session and clears the cookie
Method 3: Static Token (for server-to-server)
In app.conf:
api_token = a-long-secret-string-that-never-expires
On every request:
Authorization: Bearer a-long-secret-string-that-never-expires
The static token never expires. To invalidate it, change the value in app.conf and restart.
Custom Authentication Table
If your users are in a different table (e.g. employees):
auth_table = employees auth_columns = email;employee_code auth_pass_column = password_hash auth_salt_column = salt
The server will search the employees table by the email or employee_code column and verify the password using password_hash with salt from salt.
8. License & Demo Mode
GopServer operates in two modes: Demo Mode (no license) and Full Mode (with a valid license). The license is tied to the hardware of the machine running the server and cannot be transferred to another computer.
Demo Mode (no license or invalid key)
| Operation | Demo Mode | Full Mode |
|---|---|---|
GET /api/v1/{table} | MAX 3 records (ignores "limit") | Full pagination (up to max_page_limit) |
GET /api/v1/{table}/{id} | Works normally | Works normally |
POST /api/v1/{table} | DISABLED (HTTP 403) | Works normally |
PUT /api/v1/{table}/{id} | DISABLED (HTTP 403) | Works normally |
DELETE /api/v1/{table}/{id} | DISABLED (HTTP 403) | Works normally |
All GET responses in demo mode include extra fields:
{
"license_warning": "Demo mode: results limited to 3 records. POST, PUT and DELETE are disabled.",
"hwid": "a3f7c2d1e8b5..." // your hardware code — needed for activation
}
Activation Steps
Step 1 — Get your server's HWID (hardware fingerprint)
POST /activate
Response:
{
"hwid": "a3f7c2d1e8b5f9c2d7e4a1b6c3f8d5e2a9b4c7d1e6f3a8b5c2d9e4f7a2b6c3d8",
"nota": "Send the HWID code to the developer to receive the license key."
}
curl -X POST http://localhost:8080/activate
Step 2 — Send the HWID to the vendor
Copy the value from the hwid field and send it via email or chat to the vendor.
Step 3 — Receive your license key
The vendor will send you a unique license key generated specifically for your hardware.
Step 4 — Configure the key in conf/app.conf
license_key = MEYCIQDx... # the key received from the vendor
Save the file and restart the server.
Step 5 — Verify activation
At startup, logs/app.log should contain:
License validated successfully. Server running in full mode.
If the warning still appears, check that:
- The key was copied correctly (no extra spaces at start or end)
- You are running the server on the same machine for which the key was generated
- You restarted the server after modifying
app.conf
• Moving to a different server → you need a new key
• Reinstalling the OS → HWID usually stays the same
• Major hardware change (motherboard) → HWID may change, contact the vendor
9. API Reference
Base URL: http://your-server:8080
Health Check (public, no authentication)
{
"success": true,
"status": "Server operational",
"db": "Database connection: OK"
}
Use this endpoint for monitoring and load balancer health checks.
Authentication Endpoints
Universal CRUD (requires authentication)
{table} = exact name from conf/tables.json (lowercase)
{id} = primary key value (usually a number)
Raw SQL Queries (requires allow_raw_query = true)
Allows executing any SELECT query directly on the database: JOINs, subqueries, IN(), GROUP BY, HAVING, aggregates, functions, etc.
Security restrictions applied automatically:
- First instruction must be
SELECT(INSERT/UPDATE/DELETE rejected) - The
;character is forbidden (prevents stacked queries) - Requires authentication (JWT / session / static token)
- Endpoint is completely disabled when
allow_raw_query = false
tables.json. Enable only in development or when all authenticated users have full read access.
Response Format
List (GET /table)
{
"success": true,
"data": [ {...}, {...}, ... ],
"total_count": 150,
"page": 1,
"limit": 10,
"total_pages": 15
}
Single record (GET /table/5)
{ "success": true, "data": { "id": 5, "name": "Product X", ... } }
Create (POST) — HTTP 201
{ "success": true, "message": "Created successfully", "id": 42 }
The key in the response matches the configured primary key (e.g. "order_id").
Update (PUT) — HTTP 200
{ "success": true, "message": "Updated successfully", "rows_affected": 1 }
Delete (DELETE) — HTTP 200
{ "success": true, "message": "Deleted successfully", "rows_affected": 1 }
Error Response
{ "success": false, "error": "Error message" }
| HTTP Code | Meaning |
|---|---|
200 | OK |
201 | Created successfully (POST) |
400 | Invalid data sent by client |
401 | Unauthenticated |
403 | Forbidden (license / allow_post=0 etc.) |
404 | Table or record not found |
429 | Too many login attempts (rate limiting active) |
500 | Internal server error |
10. Filtering, Sorting & Pagination
All parameters are sent as query string in the URL.
Pagination
?limit=20 # Records per page (default 10, max = max_page_limit from app.conf) ?page=2 # Current page (default 1)
Example: GET /api/v1/products?limit=20&page=3
Sorting
?sortby=name # Sort by "name" column (default: PK column) ?order=desc # Direction: asc (default) or desc
Example: GET /api/v1/products?sortby=price&order=desc
Filtering
Simple filter (exact match):
?column=value
Example: GET /api/v1/products?category=Electronics
Filter with operator:
?column__operator=value
| Operator | Meaning | Example |
|---|---|---|
| (none) | Exact match | ?name=John |
exact | Exact match (explicit) | ?name__exact=John |
iexact | Case-insensitive match | ?email__iexact=JOHN@X.COM |
contains | Contains string (LIKE %val%) | ?name__contains=book |
icontains | Contains, case-insensitive | ?name__icontains=book |
gt | Greater than (>) | ?price__gt=100 |
gte | Greater than or equal (>=) | ?price__gte=100 |
lt | Less than (<) | ?stock__lt=10 |
lte | Less than or equal (<=) | ?stock__lte=10 |
startswith | Starts with | ?code__startswith=PRD |
istartswith | Starts with, case-insensitive | ?code__istartswith=prd |
endswith | Ends with | ?email__endswith=@com |
iendswith | Ends with, case-insensitive | ?email__iendswith=@COM |
isnull | Is NULL (true/1) or NOT NULL (false/0) | ?deleted_at__isnull=1 |
Filters can be combined:
GET /api/v1/products?category=Electronics&price__lte=500&stock__gt=0
?id___gt=10That is: two underscores for the operator separator + one that is part of the column name.
Distinct Values (for dropdowns)
?distinct=column # Returns unique values of the column ?distinct=category&all=1 # Prepends "All" as the first item
Response:
{
"success": true,
"data": [
{ "category": "Electronics" },
{ "category": "Furniture" },
{ "category": "Textiles" }
],
"count": 3
}
Combining Parameters
GET /api/v1/products?category=Electronics&price__lte=1000&sortby=price&order=asc&limit=5&page=1
11. curl Examples
In all examples below, replace TOKEN with the JWT obtained at login.
Health Check
curl http://localhost:8080/health
Login
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"mypassword123"}'
Get List with Filtering & Pagination
curl "http://localhost:8080/api/v1/products?category=Electronics&price__lte=500&limit=20&page=1&sortby=price&order=asc" \
-H "Authorization: Bearer TOKEN"
Get Single Record
curl http://localhost:8080/api/v1/products/42 \
-H "Authorization: Bearer TOKEN"
Create Record
curl -X POST http://localhost:8080/api/v1/products \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"code":"EL-001","name":"55 inch TV","category":"Electronics","price":1999.99,"stock":5,"unit":"pcs"}'
Response:
{ "success": true, "message": "Created successfully", "id": 43 }
Partial Update (only send fields to change)
curl -X PUT http://localhost:8080/api/v1/products/43 \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"price":1799.99,"stock":8}'
Delete Record
curl -X DELETE http://localhost:8080/api/v1/products/43 \
-H "Authorization: Bearer TOKEN"
Distinct Values (for dropdown)
curl "http://localhost:8080/api/v1/products?distinct=category&all=1" \
-H "Authorization: Bearer TOKEN"
Session Login (browser-based apps)
curl -c cookies.txt -X POST http://localhost:8080/api/v1/auth/login-session \
-H "Content-Type: application/json" \
-d '{"email":"john@example.com","password":"mypassword123"}'
# Request with session (cookie sent automatically):
curl -b cookies.txt http://localhost:8080/api/v1/products
# Logout:
curl -b cookies.txt -c cookies.txt -X POST http://localhost:8080/api/v1/auth/logout
Static Token (no login, no expiry)
curl http://localhost:8080/api/v1/products \
-H "Authorization: Bearer your_static_token_from_app_conf"
Raw SQL — JOIN example
curl -G http://localhost:8080/api/v1/raw \
-H "Authorization: Bearer TOKEN" \
--data-urlencode "q=SELECT o.order_id, o.order_date, o.total, u.name, u.email
FROM orders o
JOIN users u ON u.id = o.client_id
WHERE o.status = 'delivered'
ORDER BY o.order_date DESC"
Raw SQL — Aggregation
curl -G http://localhost:8080/api/v1/raw \
-H "Authorization: Bearer TOKEN" \
--data-urlencode "q=SELECT category,
COUNT(*) AS product_count,
SUM(price * stock) AS stock_value,
AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING SUM(price * stock) > 1000
ORDER BY stock_value DESC"
Raw SQL — Filter with IN()
curl -G http://localhost:8080/api/v1/raw \
-H "Authorization: Bearer TOKEN" \
--data-urlencode "q=SELECT * FROM products WHERE category IN ('Electronics','Furniture','Textiles')"
12. Security Checklist
Required Before Going to Production
- Change
jwt_secretinapp.confto a strong random value:
openssl rand -hex 32
Keep the secret safe — anyone who knows it can forge valid tokens. - Set
runmode = prodinapp.conf(logs go to file only, not console) - Use HTTPS — put a reverse proxy (Nginx, Caddy) in front of GopServer
- Restrict firewall access to port 8080; expose only HTTPS (443)
- Create a dedicated database user with minimum permissions:
PostgreSQL
CREATE USER app_user WITH PASSWORD 'strong_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
MySQL / MariaDB
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'localhost';
MSSQL
CREATE LOGIN app_user WITH PASSWORD = 'strong_password'; CREATE USER app_user FOR LOGIN app_user; GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO app_user;
Oracle
CREATE USER app_user IDENTIFIED BY strong_password; GRANT CREATE SESSION TO app_user; GRANT SELECT, INSERT, UPDATE, DELETE ON schema_owner.table_name TO app_user;
What is Protected Automatically
- hidden fields from
tables.jsonnever appear in GET responses - hidden fields cannot be set via POST (e.g. password, salt)
- locked fields cannot be modified via PUT — server returns HTTP 400 with the list of blocked fields
- The primary key cannot be modified via PUT and cannot be set via POST
- All fields sent in filters/sorting/update are validated against the permitted column list (SQL injection is not possible through these mechanisms)
- Tables not in
tables.jsonare completely inaccessible via API allow_post / allow_put / allow_delete = 0means the operation returns HTTP 403- Login endpoints are protected against brute-force attacks (configurable rate limiting)
Things to Watch Out For
- PUT allows updating
hiddenfields if they are NOT also inlocked. If you want to block a field completely: add it to bothhiddenandlocked. - Built-in rate limiting protects only the authentication endpoints. For limiting other API endpoints, use a reverse proxy (Nginx, Caddy) with rate limiting modules.
- The server does not perform type validation (e.g. does not verify that "price" is a number). This validation must be done in the frontend or a custom middleware layer.
13. Start & Stop Server
Starting
# Windows: gopserver.exe # Linux / macOS: ./gopserver
At startup, the server logs (and prints to console in dev mode):
- Language loaded
- Number of tables loaded from
tables.json - Database type and connection confirmation
- Port it is listening on
If the server doesn't start, check:
logs/app.logfor the detailed error messagedb_type,db_server,db_port,db_user,db_password,db_nameinapp.confconf/tables.jsonmust be valid JSON (use jsonlint.com)- Port 8080 must not be in use by another application
Running as a Windows Service
Using NSSM (Non-Sucking Service Manager — download from nssm.cc):
nssm install GopServer C:\path\to\gopserver.exe nssm set GopServer AppDirectory C:\path\to\ nssm start GopServer
Running as a Linux Service (systemd)
Create /etc/systemd/system/gopserver.service:
[Unit] Description=GopServer API After=network.target [Service] Type=simple User=gopserver WorkingDirectory=/opt/gopserver ExecStart=/opt/gopserver/gopserver Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
sudo systemctl daemon-reload sudo systemctl enable gopserver sudo systemctl start gopserver sudo systemctl status gopserver
Stop & Restart
# Windows: Ctrl+C in console, or: taskkill /F /IM gopserver.exe # Linux: Ctrl+C, or: systemctl stop gopserver
conf/ (app.conf, tables.json, messages.json) requires a server restart to take effect.
14. Frequently Asked Questions
Can I add a new table without recompiling?
Yes. Add a block to conf/tables.json, restart the server. Done.
How do I update a user's password?
It depends on your tables.json configuration:
- If
passwordandsaltare only inhidden(notlocked): they can be updated via PUT:
PUT /api/v1/users/5with body{"password": "new_sha512_hash", "salt": "new_salt"} - If
passwordandsaltare in bothhidden+locked: they cannot be changed via API at all. Update directly in the database or via a dedicated endpoint in your application.
Recommended for maximum security: put password and salt in both lists and manage password changes through a dedicated endpoint with additional validations (current password, confirmation, etc.).
Can I use any table and column name?
Yes, as long as the names are valid in the chosen database (letters, digits, underscore). Avoid spaces and special characters in column names.
The JWT token has expired. What do I do?
Make a new POST /api/v1/auth/login request to get a new token valid for 24 hours.
Can I change the database engine without recompiling?
Yes. Change db_type in app.conf (postgres / mysql / sqlite / mssql / oracle) and restart the server. All drivers are included in the executable. Make sure to update db_server, db_port, db_user, db_password and db_name for the new database. The table structure in conf/tables.json must match the schema of the new database.
Can I expose two databases simultaneously?
No. One GopServer instance works with a single database. For two databases, start two instances on different ports.
Does GET return all fields including sensitive ones?
No. Fields in the hidden section (e.g. password, salt) are excluded from all GET responses, even if they exist in the database.
How do I filter by multiple values of the same column? (SQL IN)
Standard URL filters do not support multiple values per column. Use the raw SQL endpoint (if allow_raw_query = true):
curl -G http://localhost:8080/api/v1/raw \
-H "Authorization: Bearer TOKEN" \
--data-urlencode "q=SELECT * FROM products WHERE category IN ('Electronics','Furniture')"
When should I use raw SQL instead of standard filters?
Use GET /api/v1/raw?q=... when you need:
- Filtering with multiple values for the same column (IN)
- JOIN between tables (e.g. orders with customer data)
- Aggregations: COUNT, SUM, AVG, GROUP BY, HAVING
- Subqueries
- Complex SQL expressions that standard filters don't support
Requirement: allow_raw_query = true in app.conf (disabled by default).
What happens if tables.json has invalid JSON?
The server will not start and will write the error to logs/app.log. Check the JSON syntax (commas, quotes, brackets) using jsonlint.com.
How do I block DELETE on an important table?
Set "allow_delete": 0 (or omit it) in conf/tables.json for that table. Any DELETE request will be rejected with HTTP 403.
Can I add role-based authorization?
GopServer authenticates the user but does not manage per-role permissions. Authorization logic (who can do what) must be implemented in the frontend application or a custom middleware layer.
Does the server support HTTPS?
Not directly. Put a reverse proxy (Nginx, Caddy, Apache) with an SSL certificate in front of the server. This is the recommended practice.
Do logs rotate automatically?
Yes. Default configuration: max file size 10MB, kept for 28 days, with automatic rotation.
How do I know if the server is running correctly?
Access http://localhost:8080/health — the response should be:
{ "success": true, "status": "Server operational", "db": "Database connection: OK" }
Quick Configuration Checklist
- Edit
conf/app.conf: db_type, db_server, db_port, db_user, db_password, db_name - Set a strong random
jwt_secret(min 32 chars) - Configure
auth_table,auth_columns,auth_pass_column - Create
conf/tables.jsonwith your table definitions - Set
runmode = prodinapp.conffor production - Set
allow_raw_query = falsein production - Configure
auth_max_attemptsandauth_lockout_seconds - Place GopServer behind an HTTPS reverse proxy
- Restrict firewall to allow only port 443 from outside
- Create a dedicated DB user with minimum permissions (no DROP, CREATE, TRUNCATE)
- Set up GopServer as a system service (NSSM on Windows, systemd on Linux)
- Add license_key to
app.confafter receiving it from vendor - Verify
GET http://your-server/healthreturns OK