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.

PostgreSQL MySQL / MariaDB SQLite MSSQL Oracle JWT Auth Session Auth Multi-language

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

2. System Requirements

EngineMinimum VersionDefault PortNotes
PostgreSQL12+5432
MySQL / MariaDB5.7+ / 10.3+3306
SQLite3.xFile-based, no server needed
Microsoft SQL Server2012+1433
Oracle Database12c Release 2+1521db_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

FilePurposeEdit without recompile?
gopserver.exeServer executable
conf/app.confMain configuration (DB, JWT, auth, ports)Yes (restart required)
conf/tables.jsonTable definitions exposed via APIYes (restart required)
conf/messages.jsonAPI messages and translationsYes (restart required)
logs/app.logApplication log fileRead-only
All files in conf/ are plain text
Edit them with any text editor. Changes take effect after a server restart.

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 valueServerDefault PortNotes
postgresPostgreSQL5432
mysqlMySQL / MariaDB3306
sqliteSQLite (local file)db_name = file path
mssqlMicrosoft SQL Server1433
oracleOracle Database1521db_name = service name
SQLite Special Case
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.
Oracle Note
db_name must be the Oracle service name (not SID). Example: db_name = XEPDB1

Authentication Settings

What this authentication is for
The settings below configure API token generation only — they define which database table and columns GopServer uses to validate credentials and issue JWT tokens.

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." }
Behind a Reverse Proxy
If GopServer is behind Nginx or Caddy, make sure the proxy forwards the real client IP via 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
Best Practice
Using environment variables keeps 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
  }
}
FieldDescriptionDefault
pkPrimary key column — used for GET/PUT/DELETE by IDid_
columnsAll columns to expose via API. Order determines response field order.Required
hiddenExcluded from GET responses AND cannot be set via POST (e.g. password, salt)[]
lockedCannot be modified via PUT once created (e.g. created_at, unique_code)[]
allow_post1 = allow record creation (POST); 0 = blocked0
allow_put1 = allow record update (PUT); 0 = blocked0
allow_delete1 = allow record deletion (DELETE); 0 = blocked0
GET is always allowed
If a table appears in tables.json, GET (read) is always available. allow_post, allow_put, allow_delete control write operations only.

Field Access Matrix

Field TypeGETPOSTPUT
normalYESYESYES
hiddenNONOYES
lockedYESYESNO
hidden + lockedNONONO

Common pattern: Put password and salt in both hidden and locked to make them completely inaccessible via API.

Rules

  1. Columns in hidden must also appear in columns
  2. Columns in locked must also appear in columns
  3. The pk column must appear in columns
  4. Column names must exactly match the database column names (case-sensitive, usually lowercase)
  5. Any column absent from columns is 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

  1. Make sure the table exists in the database
  2. Open conf/tables.json
  3. Add a new block (remember the comma after the previous block)
  4. Save the file
  5. 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"
  }
}
PlaceholderType
%sString
%dInteger
%vAny type (used in technical log errors)
%fDecimal number

How to Add a New Language (e.g. French)

  1. Open conf/messages.json
  2. 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
}
  1. In conf/app.conf set: language = FR
  2. Restart the server
Fallback language
If a key is missing from the configured language, the server automatically falls back to English (EN).

7. Authentication

The server supports 3 authentication methods, checked in this order:

  1. Static token (api_token in app.conf) — for external services
  2. JWT Bearer token — for mobile apps and SPAs
  3. 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...
Token Expiry
JWT tokens are valid for 24 hours. After expiry, you must login again to get a new token.

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)

OperationDemo ModeFull Mode
GET /api/v1/{table}MAX 3 records (ignores "limit")Full pagination (up to max_page_limit)
GET /api/v1/{table}/{id}Works normallyWorks 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."
}
This route requires no authentication
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:

License is Hardware-Bound
The license is valid only on the machine (physical or virtual) where the HWID was calculated.
• 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)

GET /health PUBLIC
{
  "success": true,
  "status": "Server operational",
  "db": "Database connection: OK"
}

Use this endpoint for monitoring and load balancer health checks.

Authentication Endpoints

POST /api/v1/auth/login JWT login
POST /api/v1/auth/login-session Session cookie login
POST /api/v1/auth/logout Destroy session

Universal CRUD (requires authentication)

GET /api/v1/{table} List records (paginated)
GET /api/v1/{table}/{id} Single record by ID
POST /api/v1/{table} Create new record
PUT /api/v1/{table}/{id} Partial update (only sent fields are modified)
DELETE /api/v1/{table}/{id} Delete record

{table} = exact name from conf/tables.json (lowercase)
{id} = primary key value (usually a number)

Raw SQL Queries (requires allow_raw_query = true)

GET /api/v1/raw?q={url_encoded_sql} Execute arbitrary SELECT

Allows executing any SELECT query directly on the database: JOINs, subqueries, IN(), GROUP BY, HAVING, aggregates, functions, etc.

Security restrictions applied automatically:

Security Warning
When enabled, any authenticated user can SELECT from any table, including tables not declared in 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 CodeMeaning
200OK
201Created successfully (POST)
400Invalid data sent by client
401Unauthenticated
403Forbidden (license / allow_post=0 etc.)
404Table or record not found
429Too many login attempts (rate limiting active)
500Internal 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
OperatorMeaningExample
(none)Exact match?name=John
exactExact match (explicit)?name__exact=John
iexactCase-insensitive match?email__iexact=JOHN@X.COM
containsContains string (LIKE %val%)?name__contains=book
icontainsContains, case-insensitive?name__icontains=book
gtGreater than (>)?price__gt=100
gteGreater than or equal (>=)?price__gte=100
ltLess than (<)?stock__lt=10
lteLess than or equal (<=)?stock__lte=10
startswithStarts with?code__startswith=PRD
istartswithStarts with, case-insensitive?code__istartswith=prd
endswithEnds with?email__endswith=@com
iendswithEnds with, case-insensitive?email__iendswith=@COM
isnullIs 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
Columns with underscore suffix (e.g. "id_")
Add the operator with double underscore: ?id___gt=10
That 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

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

Things to Watch Out For

13. Start & Stop Server

Starting

# Windows:
gopserver.exe

# Linux / macOS:
./gopserver

At startup, the server logs (and prints to console in dev mode):

If the server doesn't start, check:

  1. logs/app.log for the detailed error message
  2. db_type, db_server, db_port, db_user, db_password, db_name in app.conf
  3. conf/tables.json must be valid JSON (use jsonlint.com)
  4. 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
No hot-reload
Any change to files in 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:

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:

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