Docs
ExtensionsCore

Configuration

The core config files that tailor licenses, records, and logs.

Beyond the main config/config.json, core keeps a few self-healing files in config/core/. You usually will not need to touch these, but they are there when you do. Restart Kora after editing.

FileControls
licenses.jsonThe format of generated keys (for example XXXXX-XXXXX-XXXXX-XXXXX-XXXXX), the statuses a license can have, and the validation response template.
customers.jsonExtra fields stored on each customer (email, Discord id, ...).
products.jsonExtra fields on each product, plus reusable product templates.
addons.jsonExtra fields stored on each addon (such as a price).
logs.jsonWhether the request and activity logs are on, how many rows each keeps, and what each request captures.
geo.jsonWhere Kora downloads the GeoLite2 databases used for geo lookups and geo rules.

These files are created for you and self-heal, just like the main config. See the site-wide Configuration guide for more, and the main config/config.json reference for the app-level settings.

Geo

config/core/geo.json controls the GeoLite2 databases Kora uses to turn a request's IP into a country, region, and network (ASN). These power the geo columns in the request log and the country, region, and asn rule types.

SettingDefaultDescription
ENABLEtrueDownload and load the geo databases on startup.
DATABASE_URL(Zero bucket)URL of the GeoLite2 City database (.mmdb), used for country and region.
ASN_URL(Zero bucket)URL of the GeoLite2 ASN database (.mmdb).

Kora downloads each database once into a geo/ folder and reads it locally, so lookups are offline and fast after the first start. If a database can't be downloaded or loaded, geo lookups return empty and geo rules are skipped, so nothing breaks.

The databases are free from MaxMind. Host the two .mmdb files anywhere Kora can reach and point DATABASE_URL / ASN_URL at them.

Request logs

config/core/logs.json holds the request and activity log settings under LOGS.

SettingDefaultDescription
REQUESTS.ENABLEtrueRecord incoming API requests.
REQUESTS.RETENTION5000How many request rows to keep.
REQUESTS.GEOtrueAdd country, region, and ASN to each request (needs the geo databases above).
REQUESTS.BODYtrueStore the request body, truncated to 8KB.
REQUESTS.HEADERStrueStore the request headers. Sensitive ones (authorization, cookie, ...) are redacted.
ACTIVITY.ENABLEtrueRecord licensing activity (validations, rejections, ...).
ACTIVITY.RETENTION5000How many activity rows to keep.

Each request row is expandable on the Requests dashboard page to show its location, network, headers, and body.

License statuses

config/core/licenses.json under LICENSES.STATUS defines the statuses a license can hold and how they behave. A license's status is checked on every validation: a negative status rejects the key and returns its message as the reason.

SettingDefaultDescription
DEFAULTACTIVEThe status a new license gets when you don't set one. Must be one of the TYPES.
TYPES(list)Every status a license may have. Assigning a status outside this list is rejected.
LINK(map)Wires the built-in behaviors (active, disabled, expired, expiring) to your status names.

Each entry in TYPES is:

FieldDescription
NAMEThe status name, used when you create or edit a license.
POSITIVITYtrue if the key passes validation, false to reject it.
MESSAGEThe message returned on validation. For a negative status, this is the rejection reason.

LINK maps the four built-in behaviors onto status names, so you can rename them:

KeyDescription
ACTIVE.STATUSThe normal, valid status.
DISABLED.STATUSThe status used for a manually disabled key.
EXPIRED.STATUSThe status a license reports once its expires_at has passed. Validation is rejected.
EXPIRING.STATUSThe status a still-valid license reports when it is within EXPIRING.WINDOW of expiring.
EXPIRING.WINDOWHow long before expiry the EXPIRING status kicks in (for example 7d).

Expiry is resolved automatically from the license's expires_at: a key past its expiry reports the EXPIRED status and fails validation, while one inside the EXPIRING window reports the EXPIRING status but still validates.

"STATUS": {
    "DEFAULT": "ACTIVE",
    "LINK": {
        "ACTIVE": { "STATUS": "ACTIVE" },
        "DISABLED": { "STATUS": "DISABLED" },
        "EXPIRED": { "STATUS": "EXPIRED" },
        "EXPIRING": { "STATUS": "EXPIRING", "WINDOW": "7d" }
    },
    "TYPES": [
        { "POSITIVITY": true,  "NAME": "ACTIVE",    "MESSAGE": "License is valid & active!" },
        { "POSITIVITY": false, "NAME": "SUSPENDED", "MESSAGE": "License has been suspended." },
        { "POSITIVITY": false, "NAME": "DISABLED",  "MESSAGE": "License has been manually disabled." }
    ]
}

Durations use a number plus a unit: ms, s, m, h, or d.

Custom validation response

By default, license validation returns a fixed JSON shape. If your product expects a different shape, turn on a custom response in config/core/licenses.json under LICENSES.VALIDATION and build the JSON yourself with placeholders.

SettingDefaultDescription
CUSTOMfalseUse your VALID / INVALID templates instead of the built-in response.
VALID(template)The JSON returned when a key is valid.
INVALID(template)The JSON returned when a key is rejected.

Any string in a template is rendered with the placeholders below. A value that is exactly one placeholder keeps its real type (so "%ADDONS%" becomes an array, not a string); placeholders inside a longer string are substituted as text.

PlaceholderValue
%VALID%true or false.
%MESSAGE%The status or rejection message.
%KEY%The license key.
%PRODUCT%The product name.
%CUSTOMER%The customer.
%STATUS%The status name (for example ACTIVE).
%EXPIRES_AT%The expiry timestamp.
%ADDONS%The addons array.
%USER_ID%The linked user id, if any.
%VARIABLES%The resolved variables object.
%USER_VARIABLES%The license's user variables object.
%SESSION%The session object ({ token }) when the Sessions extension issues one, otherwise null.
%TIMESTAMP%The response time in milliseconds.
"VALIDATION": {
    "CUSTOM": true,
    "VALID": {
        "success": "%VALID%",
        "plan": "%PRODUCT%",
        "expires": "%EXPIRES_AT%",
        "features": "%VARIABLES%"
    },
    "INVALID": {
        "success": "%VALID%",
        "reason": "%MESSAGE%"
    }
}

On this page