Environments policy — dev / stg / prd

Why this policy exists. During development, web pages are published at public internet addresses for review and iteration. Without an access control layer, those pages can be indexed by search engines, scraped by AI training crawlers, and shared (intentionally or accidentally) before they're ready. Branding, naming, copy, and visual identity are still in flux during dev — premature exposure creates SEO debt, brand contamination, and reputation risk that's expensive to undo later.

This policy defines what environment a deployment belongs to, and what access controls apply at each level.

Scope

Applies to every web surface published from the Koder Stack to a public internet address:

  • Landing pages (product, sector, area, institutional, catalog, spec)
  • Web apps (admin, dashboards, consoles, Hub client, etc.)
  • Documentation sites
  • Developer-facing portals
  • API gateway pages (auth flows, callbacks)
  • Status / observability dashboards
  • Any subdomain of koder.dev or any other Koder-controlled domain

Does not apply to:

  • Local dev (localhost, .local, .lan)
  • VPN-only internal services not bound to public IPs
  • Email-only services

Environment classification

Three environments per meta/context/infrastructure/environments.md:

Environment Code Purpose Domain pattern Audience
Development dev Active development, iteration on namingdesigncopy dev.<slug>.koder.dev Author + invited reviewers
Staging stg Pre-launch validation, stakeholder review stg.<slug>.koder.dev Internal team + selected externals
Production prd Public launch <slug>.koder.dev (no env prefix) Public

Every Koder Jet vhost MUST declare environment = "dev" | "stg" | "prd" in its sites.toml block. Vhosts without explicit environment declaration default to dev (fail-safe — most restrictive).

Environment codes (3-letter canonical)

The codes dev / stg / prd are the only accepted environment identifiers across the Koder Stack. Always 3 letters, always lowercase. Used uniformly in:

Surface Format
URL prefix (dev/stg only) dev.<slug>.koder.dev, stg.<slug>.koder.dev
sites.toml environment field "dev", "stg", "prd"
Env vars KODER_ENV=dev, KODER_ENV=stg, KODER_ENV=prd
Branch names, log fields, métricas, dashboards dev, stg, prd
Hostnames / LXC names s.<slug>-dev, s.<slug>-stg, s.<slug>-prd (or no suffix for prd)

Forbidden spellings: staging (use stg), production / prod (use prd), development (use dev). Spelled-out forms are acceptable only in human-readable prose (e.g., headings, narrative description) — never in machine-consumed fields.

Production is the only env without a URL prefix — prd appears in configlabelsenv vars but never in the address bar.

Access control rules

Dev environment

Layer Required? Mechanism
Auth gate (email approval flow) Required Inbound IP must be in 24h whitelist OR trigger email approval flow to admin (see § Email approval flow)
noindex / nofollow Required <meta name="robots" content="noindex, nofollow"> + X-Robots-Tag: noindex, nofollow header + robots.txt Disallow: /
Sitemap Forbidden sitemap.xml MUST NOT be served from dev
dev. subdomain prefix Strongly recommended URL itself signals environment in any screenshot that includes the address bar
Visible UI banner Forbidden by default Banner pollutes design evaluation, the primary activity in dev. Activatable per-vhost only with explicit justification (e.g., demo to unaligned external stakeholder)
Semantic markup Recommended <meta name="koder-env" content="dev"> and X-Koder-Environment: dev header for tooling
Geofencing Optional Pre-filter at gate: block IPs outside Brazil before triggering email flow (reduces approval spam from bot scans)

Staging environment

Layer Required? Mechanism
Auth gate Required Same email approval flow as dev, but with longer TTL (7 days) and more permissive whitelist (allow Koder ID-authenticated users without per-IP approval)
noindex / nofollow Required Same as dev
stg. subdomain prefix Required Mandatory in stg
Visible UI banner Optional More acceptable here than dev (review activity is less about pure visual evaluation, more about end-to-end flow validation)

Prod environment

Layer Required? Mechanism
Auth gate Forbidden by default Prod is public. Opt-in only for specific endpoints that need auth (admin panels, etc. — those use Koder ID directly, not the email approval flow)
noindex Forbidden by default Prod opts into indexing. Per-page noindex allowed for legitimate reasons (login pages, admin)
Sitemap Required sitemap.xml published per spec
HTTPS Required Per policies/security.kmd

Email approval flow (dev/stg)

When a request comes in from a non-whitelisted IP to a dev/stg surface:

1. Visitor → kora.dev.koder.dev (any path)
   ↓ Koder Jet auth gate intercepts
2. Gate responds with 403 + access-request page:
   "Access pending. An approval request has been sent to the
    site administrator. Reload in 30s to retry."
   ↓ Gate enqueues approval request
3. Email sent to configured admin (default: rpm32510@gmail.com):
   Subject: "[Koder Access] kora.dev.koder.dev"
   Body (English):
     "Access requested.
      URL: https://kora.dev.koder.dev/<path>
      Origin IP: 177.x.x.x  (Geo: BR/SP)
      User-Agent: <ua>
      Timestamp: 2026-05-02 14:30 BRT

      [Review request →]"
   The link goes to confirm.koder.dev/req/<token> — does NOT auto-approve.
   ↓
4. Admin clicks link on phone/desktop → loads confirmation page (auth via Koder ID).
   Page shows request details + two buttons:
     [✅ APPROVE — 24h]   [❌ DENY]
   Buttons are POST requests with CSRF token. Form action is idempotent
   per-token (cannot re-vote on same request).
   ↓
5. On APPROVE: gate adds origin IP to whitelist with TTL = 24h (dev) or
   7d (stg). Visitor's next request passes.
   On DENY: visitor's next request continues to see 403.
   ↓
6. After TTL expires, IP is removed from whitelist automatically.

The whitelist is FLEET-WIDE — keyed by IP, not by (page, IP) (jet#224). An approval (or a permanent_whitelist match) authorizes the IP on every dev/stg surface, not just the page the request came from. Approving once is enough; the visitor never re-approves per page. Rationale: the gate is an anti-bot / design-eval-privacy boundary, not a secret boundary (real secrets go through Koder ID and are never exposed on a dev page), so a per-IP grant matches the threat model and removes per-page friction. The approval record keeps the originating page as an audit field (via) only. An earlier implementation keyed the dynamic whitelist by (host, ip) and re-prompted the same IP on each new vhost — that was drift from this section's own language ("adds origin IP to whitelist"), corrected in jet#224.

Why no clickable buttons in the email itself

Email scanners (Gmail, Bitdefender, Microsoft Defender, etc.) prefetch all links to scan for malware. If the email contained "Yes/No" buttons as GET-request links, the scanner would auto-trigger them before the user sees the email. Approval would happen 100% automatically — defeating the purpose.

Approval MUST happen via a POST request from a deliberate human click on a confirmation page that is itself behind authentication. The email provides only a single neutral link to that confirmation page.

This pattern matches GitHub's permission requests, Vercel's preview deployment auth, AWS account access requests, etc.

Rate limiting (anti-spam)

  • Per origin IP: max 1 approval-request email per 10 minutes
  • Per admin inbox: max 10 pending requests at any time (additional

    requests get auto-deferred message, not email)

  • Per token: single-use; once approved or denied, token is consumed
  • Scanner protection: confirmation page requires human interaction

    (Koder ID session + CSRF token + POST)

Pre-filter at gate: requests from outside Brazil get a passive 403 without triggering the email flow. This prevents bot-scan noise from flooding the admin's inbox.

Configurable per-vhost. Default ON for dev, OFF for stg.

Permanent whitelist (admin convenience)

Per-vhost configuration may declare permanent IPs (admin's office, home network) that bypass the approval flow:

[sites."kora.dev.koder.dev"]
environment = "dev"
permanent_whitelist = ["177.123.45.67/32", "192.168.1.0/24"]

These IPs never trigger the email flow. They're effectively pre-approved.

Global permanent whitelist (jet#225). An IP that should be trusted on every dev/stg surface (the owner's fixed office IP, a CI egress range) is declared once in the top-level [auth_gate] block instead of being repeated in every vhost:

[auth_gate]
permanent_whitelist = ["189.91.64.67/32"]

The global list is merged additively with each vhost's [sites.auth_gate] permanent_whitelist (both apply; global does not replace per-vhost). Prefer the global block for fleet-wide trusted IPs; keep per-vhost entries only for IPs that should reach a single surface. This is the static-side complement to the fleet-wide dynamic whitelist (jet#224).

R — Only STATIC IPs in permanent_whitelist. An entry must be a fixed, owned address (a static-IP link or a CI egress with a stable range). Never whitelist a dynamic-IP link (mobile/4G, dynamic broadband): the address rotates, so the entry is stale at the next reassignment and a security hole meanwhile — when the carrier reassigns that address from its pool, the next subscriber inherits trust on every gated surface. Access from a dynamic link (e.g. an office's TIM failover) goes through the normal email-approval flow (one-time, 24h TTL) — that is exactly what it is for. (jet#229, 2026-06-20: a dynamic TIM address had leaked into the whitelist and was correctly removed.)

Multi-approver model

V1 implementation: single approver per vhost (default rpm32510@gmail.com).

V2 (planned): approver can be any account with admin role on the relevant Koder ID tenant. Email goes to all admins; first to approve wins.

Implementation components

Per policies/self-hosted-first.kmd, every component is a Koder Stack self-hosted module:

Component Role
Koder Jet (infra/net/jet/) Auth gate middleware; intercepts requests, manages whitelist TTL, serves access-pending page
servicesfoundationreporter or Kmail Sends approval-request emails
Koder ID (already in production) Authenticates admin on the confirmation page
KV store (infra/data/ — kdb-next or similar) Stores whitelist entries with TTL, pending request tokens
Koder Konsul (observability) Logs approval/deny events for audit

Auto-flip on environment promotion

Environment is declared in koder.toml [deployment.environment] and the Koder Jet vhost [sites.<host>] environment field. Promoting a module from dev → stg → prd is a config change, not a code change:

  • environment = "dev" → auth gate ON, noindex ON, banner OFF
  • environment = "stg" → auth gate ON (lighter), noindex ON
  • environment = "prd" → auth gate OFF (default), noindex OFF (default)

CI/CD pipeline MUST refuse to deploy a build whose koder.toml env doesn't match the target Koder Jet vhost env (prevents accidental dev build going to prod vhost or vice versa).

Daily audit

/k-housekeep includes a daily audit that:

  1. Walks all Koder Jet vhosts in s.forge:/etc/koder-jet/sites.toml
  2. For each, verifies:
    • environment field is declared
    • dev/stg vhosts have auth gate enabled
    • dev/stg vhosts return noindex headers (HTTP HEAD probe)
    • prod vhosts do NOT have noindex (positive check)
    • sitemap.xml is absent from dev/stg
    • sitemap.xml is present from prod
  3. Reports drift to meta/context/handoff/CURRENT.md for next session

Exceptions (whitelisted)

A vhost may opt out of any rule with explicit justification in koder.toml:

[deployment]
environment = "dev"
exceptions = ["banner_required"]  # if visible banner is needed for a demo
exceptions_justification = "Demo to external stakeholder 2026-05-15"
exceptions_expire = "2026-05-20"

Exceptions auto-expire and require re-justification.

Defaults summary (the 4 scope questions, answered)

  1. Domain scope — gate applies to dev/stg only by default. Prod

    stays public. Specific prod URLs may opt into auth (admin panels, etc.) using Koder ID directly, not the email approval flow.

  2. Multi-approver eventual — V1 single approver (rpm32510@gmail.com).

    V2 plans Koder ID admin role expansion.

  3. Permanent whitelist — supported per-vhost via permanent_whitelist

    in sites.toml; bypasses email flow.

  4. Geofencing — optional, configurable per-vhost; default ON for dev

    (block non-Brazil to reduce inbox spam from scanners), OFF for stg.

Cross-references

  • meta/context/infrastructure/environments.md — physical/network

    topology of the 3 environments

  • policies/web-server.kmd — Koder Jet is the official server
  • policies/security.kmd — TLS/HTTPS requirements (apply at all envs)
  • policies/self-hosted-first.kmd — implementation must use Koder Stack

    components

Migration plan

Pre-existing public dev surfaces (everything currently at *.koder.dev that is in fact development-stage):

  1. Inventory current vhosts in s.forge:/etc/koder-jet/sites.toml
  2. Classify each: actually-dev / actually-stg / actually-prd
  3. For misclassified-as-prod-but-really-dev: add environment = "dev" and

    migrate to dev.<slug>.koder.dev subdomain; deploy auth gate

  4. For prod that should stay prod: confirm they're truly launchable; add

    environment = "prd" declaration

  5. Update DNS where subdomain changes are required

Tracked in implementation ticket projects/koder-stack/backlog/in-progress/119-environments-policy-implementation.md (orchestration) + infra/net/jet/backlog/in-progress/106-auth-gate-feature.md (gate feature) + jet#107..#114 (technical breakdown).

Lifecycle of the email-approval gate

The email-approval flow is a stack-lifecycle-bound mechanism, not a permanent architectural fixture. It is heavily used during the current acceleration phase of the Koder Stack — a phase characterized by:

  • Most vhosts under *.koder.dev being in active iteration on naming,

    design, copy, brand

  • Premature exposure carrying real SEO / brand contamination cost
  • Few vhosts having reached public-launch maturity

As more components reach homologação and graduate to environment = "prd", the gate's footprint shrinks naturally — no policy change required, just per-vhost reclassification. The expected trajectory:

Stack phase Typical vhost mix Gate usage
Acceleration (current — through homologação) mostly dev/stg heavy; default for any new vhost
Post-homologação mixed devstgprd moderate; only vhosts on uniterated surfaces
Mature stack mostly prod dormant; gate code stays available for ad-hoc dev surfaces, debug endpoints, internal APIs that need explicit approval

The mechanism is kept available indefinitely (the auth-gate code is a generally useful capability — same mechanism can protect debug endpoints, internal APIs, stg copies of prd surfaces, etc.), but its default-on-for-all-dev-vhosts behavior is tied to this phase. A future revision of this policy may remove the dev-default-required posture once the dev surface footprint shrinks below a threshold worth auditing.

Calibration trigger: when the user signals "Koder Stack is in homologação", revisit this section and consider:

  • Whether the daily audit in /k-housekeep should still flag missing

    gates as drift, or downgrade to advisory

  • Whether the environment = "prd" default for unspecified vhosts

    should be reversed (currently fail-safe; may become noise later)

  • Whether to deprecate the email-approval flow in favor of pure Koder

    ID auth on a per-vhost basis

Do not recalibrate unilaterally — wait for the user's explicit signal, analogous to the /k-go Modo D recalibration trigger documented in the slash command.

Status

Ratified — 2026-05-29 (owner). Draft since 2026-05-02; ratification was gated by the implementation ticket (projects/koder-stack#119), now complete: F1–F6 (Koder Jet auth gate + Kmail integration + whitelist store + daily audit + CI env-match) plus F7 (vhost inventory reconciled against the live sites.toml — 226 vhosts classified, residual 0; 201 confirmed carry environment= live, 168 dev gated, dead/retired surfaces decommissioned). The policy is now normative (strict) — the daily audit in /k-housekeep Fase 0.8 (audit-environments.sh over registries/environments-vhosts.md) flags drift, and the CI env-match gate blocks mismatched deploys.

Historical: this was Draft — advisory from 2026-05-02 until the F7 reconciliation closed the residual.