Appearance
Zone and profile settings
Field by field.
Field names and types below are read directly from the live tenant schema (kiwire_zone, kiwire_zone_child, kiwire_profiles), not from a form or a stale migration comment — some columns have been added, renamed, or moved into JSON since the tables were first created.
Zone fields
kiwire_zone — one row per named zone:
| Field | Type | Meaning |
|---|---|---|
name | varchar(120) | Zone name, unique per tenant. This is what guests, reports, and allowed_zone/force_allowed_zone all reference — there is no separate zone code. |
auto_login | varchar(120) | Auto-login method configured for this zone, if any. |
simultaneous | int, default 0 | Zone-wide concurrent-session cap. 0 = no limit. Enforced as check step 7 in the login pipeline (see Limits and how they combine). |
priority | int, default 0 | Tie-breaker when more than one zone-child rule could match the same connection. |
journey_id | bigint, nullable, FK → kiwire_journeys.id | The guest-facing page flow assigned to this zone. (Replaced a plain journey string column — 2026-04-02 migration.) |
force_profile | varchar(120), nullable | See Force profile below. |
force_allowed_zone | varchar(120), nullable | See Force profile below. |
is_active | boolean, default true | Inactive zones are excluded from zone dropdowns and filters across the admin (reports, profile pickers, etc). |
A zone on its own doesn't match any traffic — the matching rules live in kiwire_zone_child, one row per NAS/SSID/VLAN/IP combination that should resolve to this zone:
| Field | Type | Meaning |
|---|---|---|
zone_id | FK → kiwire_zone.id, cascade delete | |
ipaddr / ipv6addr | varchar(60) | IP-based match. |
vlan | varchar(60) | VLAN-based match. |
ssid | varchar(120) | SSID-based match. |
nasid | varchar(120) | Controller unique_id — NAS-based match. |
dzone | varchar(120) | The zone name as reported by the controller itself (device-side zone), for controllers that report their own zone concept. |
priority | int, default 0 | Match precedence when a connection matches more than one child row. |
Profile fields
kiwire_profiles — one row per plan/tier:
| Field | Type | Meaning |
|---|---|---|
name | varchar(120) | Profile name — this is also what kiwire_zone.force_profile and kiwire_auto_reset.profile store, not the id. |
type | countdown / expiration / free (char(20); the column's schema default time is never written by the form) | Which time-limit attribute the profile is built with — countdown → control:Max-All-Session, expiration → control:Access-Period, free → neither (see Limits and how they combine). A legacy pay-per-use value is remapped to expiration on edit. |
price | float, default 0 | |
upload_speed / download_speed | varchar(50), e.g. "10M" | Parsed to bits/sec (K=×1,000, M=×1,000,000, G=×1,000,000,000, decimal SI). |
quota_limit | bigint, nullable | Megabytes in the DB — converted to bytes (× 1,048,576) when building the RADIUS quota attribute. Enforced for any type whenever it is > 0. |
session_time | int, nullable | Minutes — "Maximum session duration" per the column comment. Only meaningful for countdown and expiration profiles; ignored for free. |
idle_timeout | int, nullable | Seconds. Sent as reply:Idle-Timeout — disconnects the guest after this much inactivity, independent of the time/quota type. |
grace_period | int, default 0 | Days. |
reset_type | enum: never/30min/1hour/1day/1week/1month/1year, default never | Auto-reset schedule. |
reset_schedule, reset_grace, custom_time, custom_weekly | varchar/int, nullable | Companion scheduling fields for auto-reset — custom_weekly is the week-start day (0=Sun … 6=Sat) for weekly resets, mirrored onto the live kiwire_auto_reset row for that profile. |
advance_profile | bigint, nullable, FK → another kiwire_profiles.id | The profile a session gets switched to when this one's limit is exceeded and no auto-reset applies. |
a_limit | int, nullable | Not enforced at login. A percentage cap checked live during RADIUS accounting: once mid-session usage crosses quota × a_limit / 100, the session is either CoA'd to advance_profile (if set) or disconnected outright. 0 disables it. |
attribute | JSON (text column) | Custom RADIUS attributes merged into the Access-Accept — e.g. control:Simultaneous-Use, control:Access-Period, control:Max-All-Session, reply:Acct-Interim-Interval. See RADIUS attributes. |
attribute_custom | JSON (text column) | Same idea, but always wins over both attribute and the explicit fields above — the hand-edited escape hatch. |
is_active | boolean, default true | |
is_system | boolean, default false | System profiles cannot be deleted. |
remark | text, nullable | |
created_by / updated_by | varchar(50) |
Two things that look like profile fields but aren't columns any more:
simultaneous_use— dropped from the table; now lives only insideattributeascontrol:Simultaneous-Use(default1when absent).session_timeout/shared_quota/redirect_url— all added and then dropped again in early migrations; none exist on the current table.
Force profile
kiwire_zone.force_profile and force_allowed_zone are plain strings, not foreign keys — validated against kiwire_profiles.name / kiwire_zone.name only at save time, not enforced by the database.
force_profileholds the profile's NAME. The zone edit form always writes a name. The lookup at account-creation time also accepts a bare numeric value and treats it as a profile id instead — so an older or script-written row that happens to hold a numeric id still resolves correctly. A name that no longer matches any profile resolves tonulland is silently ignored (the account still gets created, just without the forced profile) rather than blocking the request.force_allowed_zoneholds a comma-separated list of zone NAMES (a mix of names and numeric ids is also accepted, resolved the same dual way).- Effect: when an account is created while the guest's portal session is scoped to a zone with
force_profileset, that zone's profile overrides whatever the login method would otherwise have assigned — unless the caller explicitly passeszone_force_profile: false(used by the payment flow: a guest who just paid for a specific plan keeps that plan; the zone'sforce_allowed_zonerestriction still applies on top of it). Likewise, a zone withforce_allowed_zoneset overrides the new account'sallowed_zoneto that zone's resolved zone-id list. - This is a create-time override only — it doesn't retroactively change existing accounts, and it has no effect on accounts created outside that zone.
Limits and how they combine
Everything above feeds a single, strictly ordered pipeline — the login check — that runs on every login attempt. The first check that fails is the only one the guest (and the report) ever sees; nothing after it runs:
- Account exists, password verifies (skipped for EAP/CHAP — FreeRADIUS verifies those itself).
- Tenant-wide concurrent-session cap, then the licence's monthly login quota.
- Account
statusisactive. - Account's profile-policy time-restriction window (
kiwire_profile_policies), if any. - Account
date_valueisn't in the future,date_expiryisn't in the past. - Any third-party policy webhook.
- Zone: account's
allowed_zone(CSV of zone ids) includes the current zone — then the zone's ownsimultaneouscap isn't full. - Account's allowed-MAC list, if the account restricts by MAC.
- Time (
countdown/expirationprofiles): the profile'scontrol:Access-Period(expiration — elapsed time sincedate_activate) orcontrol:Max-All-Session(countdown — cumulativesession_time) isn't exhausted. - Quota (any type with
quota_limit> 0):quota_limitisn't exhausted. - The account's own
control:Simultaneous-Useconcurrent-connection cap isn't full. - (Voucher accounts only)
date_activateis stamped on this, the first successful login.
When a time or quota limit (steps 9–10) is hit, Kiwire doesn't reject immediately — it first checks for a matching kiwire_auto_reset row for that profile (exec_when = 'ot'): if found, and the account isn't still inside that reset's grace window since its last logout, usage counters are zeroed and the account reverts to its subscribed profile. Otherwise, if the profile has advance_profile set, the session is switched to that profile (with a CoA sent) instead of being rejected. Only when neither applies does the guest get TIME_LIMIT_REACHED / QUOTA_LIMIT_REACHED.
The profile's a_limit percentage is a separate, mid-session check — it isn't part of this login-time pipeline at all. It runs inside RADIUS accounting on every Interim-Update/Stop packet, so a session can be switched to advance_profile (or disconnected) between logins, purely from usage crossing the percentage threshold.