Skip to content

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:

FieldTypeMeaning
namevarchar(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_loginvarchar(120)Auto-login method configured for this zone, if any.
simultaneousint, default 0Zone-wide concurrent-session cap. 0 = no limit. Enforced as check step 7 in the login pipeline (see Limits and how they combine).
priorityint, default 0Tie-breaker when more than one zone-child rule could match the same connection.
journey_idbigint, nullable, FK → kiwire_journeys.idThe guest-facing page flow assigned to this zone. (Replaced a plain journey string column — 2026-04-02 migration.)
force_profilevarchar(120), nullableSee Force profile below.
force_allowed_zonevarchar(120), nullableSee Force profile below.
is_activeboolean, default trueInactive 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:

FieldTypeMeaning
zone_idFK → kiwire_zone.id, cascade delete
ipaddr / ipv6addrvarchar(60)IP-based match.
vlanvarchar(60)VLAN-based match.
ssidvarchar(120)SSID-based match.
nasidvarchar(120)Controller unique_id — NAS-based match.
dzonevarchar(120)The zone name as reported by the controller itself (device-side zone), for controllers that report their own zone concept.
priorityint, default 0Match precedence when a connection matches more than one child row.

Profile fields ​

kiwire_profiles — one row per plan/tier:

FieldTypeMeaning
namevarchar(120)Profile name — this is also what kiwire_zone.force_profile and kiwire_auto_reset.profile store, not the id.
typecountdown / 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.
pricefloat, default 0
upload_speed / download_speedvarchar(50), e.g. "10M"Parsed to bits/sec (K=×1,000, M=×1,000,000, G=×1,000,000,000, decimal SI).
quota_limitbigint, nullableMegabytes in the DB — converted to bytes (× 1,048,576) when building the RADIUS quota attribute. Enforced for any type whenever it is > 0.
session_timeint, nullableMinutes — "Maximum session duration" per the column comment. Only meaningful for countdown and expiration profiles; ignored for free.
idle_timeoutint, nullableSeconds. Sent as reply:Idle-Timeout — disconnects the guest after this much inactivity, independent of the time/quota type.
grace_periodint, default 0Days.
reset_typeenum: never/30min/1hour/1day/1week/1month/1year, default neverAuto-reset schedule.
reset_schedule, reset_grace, custom_time, custom_weeklyvarchar/int, nullableCompanion 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_profilebigint, nullable, FK → another kiwire_profiles.idThe profile a session gets switched to when this one's limit is exceeded and no auto-reset applies.
a_limitint, nullableNot 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.
attributeJSON (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_customJSON (text column)Same idea, but always wins over both attribute and the explicit fields above — the hand-edited escape hatch.
is_activeboolean, default true
is_systemboolean, default falseSystem profiles cannot be deleted.
remarktext, nullable
created_by / updated_byvarchar(50)

Two things that look like profile fields but aren't columns any more:

  • simultaneous_use — dropped from the table; now lives only inside attribute as control:Simultaneous-Use (default 1 when 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_profile holds 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 to null and is silently ignored (the account still gets created, just without the forced profile) rather than blocking the request.
  • force_allowed_zone holds 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_profile set, that zone's profile overrides whatever the login method would otherwise have assigned — unless the caller explicitly passes zone_force_profile: false (used by the payment flow: a guest who just paid for a specific plan keeps that plan; the zone's force_allowed_zone restriction still applies on top of it). Likewise, a zone with force_allowed_zone set overrides the new account's allowed_zone to 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:

  1. Account exists, password verifies (skipped for EAP/CHAP — FreeRADIUS verifies those itself).
  2. Tenant-wide concurrent-session cap, then the licence's monthly login quota.
  3. Account status is active.
  4. Account's profile-policy time-restriction window (kiwire_profile_policies), if any.
  5. Account date_value isn't in the future, date_expiry isn't in the past.
  6. Any third-party policy webhook.
  7. Zone: account's allowed_zone (CSV of zone ids) includes the current zone — then the zone's own simultaneous cap isn't full.
  8. Account's allowed-MAC list, if the account restricts by MAC.
  9. Time (countdown / expiration profiles): the profile's control:Access-Period (expiration — elapsed time since date_activate) or control:Max-All-Session (countdown — cumulative session_time) isn't exhausted.
  10. Quota (any type with quota_limit > 0): quota_limit isn't exhausted.
  11. The account's own control:Simultaneous-Use concurrent-connection cap isn't full.
  12. (Voucher accounts only) date_activate is 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.

Last updated:

Kiwire platform documentation