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.devor 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)
Geofencing (recommended for dev)
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 OFFenvironment = "stg"→ auth gate ON (lighter), noindex ONenvironment = "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:
- Walks all Koder Jet vhosts in
s.forge:/etc/koder-jet/sites.toml - For each, verifies:
environmentfield is declared- dev/stg vhosts have auth gate enabled
- dev/stg vhosts return
noindexheaders (HTTP HEAD probe) - prod vhosts do NOT have noindex (positive check)
- sitemap.xml is absent from dev/stg
- sitemap.xml is present from prod
- Reports drift to
meta/context/handoff/CURRENT.mdfor 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)
- 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.
- Multi-approver eventual — V1 single approver (
rpm32510@gmail.com).V2 plans Koder ID admin role expansion.
- Permanent whitelist — supported per-vhost via
permanent_whitelistin sites.toml; bypasses email flow.
- 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/networktopology of the 3 environments
policies/web-server.kmd— Koder Jet is the official serverpolicies/security.kmd— TLS/HTTPS requirements (apply at all envs)policies/self-hosted-first.kmd— implementation must use Koder Stackcomponents
Migration plan
Pre-existing public dev surfaces (everything currently at *.koder.dev that is in fact development-stage):
- Inventory current vhosts in
s.forge:/etc/koder-jet/sites.toml - Classify each: actually-dev / actually-stg / actually-prd
- For misclassified-as-prod-but-really-dev: add
environment = "dev"andmigrate to
dev.<slug>.koder.devsubdomain; deploy auth gate - For prod that should stay prod: confirm they're truly launchable; add
environment = "prd"declaration - 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.devbeing 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-housekeepshould still flag missinggates as drift, or downgrade to advisory
- Whether the
environment = "prd"default for unspecified vhostsshould 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 — advisoryfrom 2026-05-02 until the F7 reconciliation closed the residual.