Skip to content

RFC(v1): Virtual codegen modules for Next.js strict layout (env/ stays user-authored only) #1402

Description

@yamcodes

Summary

Exploratory vision for v1: keep the user-facing strict layout tree small and intentional, and move Next.js codegen artifacts out of env/ into tool-owned virtual modules under root .arkenv/.

Supersedes the file-based direction in #1304 as the preferred end state. Related: #1307 / #1401 (Nuxt virtual alias), #1403 (Next auto-extend track), ADR 0016, #1109 (dot-folder rule — not reversed).

Problem

Next.js strict layout still requires manual server wiring and puts generated factories beside editable schemas:

import arkenv from "@arkenv/nextjs/server";
import { env as clientEnv } from "./client";

export const env = arkenv(
  { DATABASE_URL: "string" },
  { extends: [clientEnv] },
);

#1304 proposed generating server.gen.ts beside env.gen.ts to fix that. Design review found deeper issues with putting more machine output under env/:

  • env.gen.ts + server.gen.ts naming is asymmetric and easy to misread
  • generated/ + .gen is redundant marking
  • Colocating *.gen.ts next to editable schemas invites misclicks
  • env/internal/ + env/generated/ together makes env/ feel like a mini-package, not an env validator
  • Awkward-path guardrails (internal/shared.ts) are a weak substitute for an explicit client-exposure API

Locked decisions

1. Phase out internal/shared.ts (concept shift)

Adapt the flat-layout exposeToClient: [...] array into strict layout. Client-safe variables are tagged on the single source of truth (e.g. env/server.ts) instead of living in a separately importable shared file. Exact transition / relationship to #1448 still under human mapping (ready for human).

2. On-disk location: root .arkenv/

Generated factories write to a root .arkenv/ directory (gitignored; regenerate via withArkEnv / arkenv generate).

Not .next/cache/arkenv — Next’s .next/cache is an ephemeral build-performance cache (CI Build Caching); the ecosystem routinely wipes .next / .next/cache to troubleshoot. Not node_modules/.arkenv/ — brittle for monorepos / edge (Prisma’s lesson).

Meta-framework parallel: .nuxt/, .svelte-kit/ are first-class tool codegen dirs; host bundler caches are a different job.

#1109 defense (canonical): #1109 forbids importable code in dot-folders. Because public access is via virtual specifiers, .arkenv/ is cache-only backing — users never import from ".arkenv/...". This does not reverse #1109.

Scaffolding: CLI must add .arkenv/ to .gitignore.

3. Module resolution via withArkEnv

Codegen outputs physical files under .arkenv/; withArkEnv maps the public specifier to that path and injects Webpack resolve.alias + Turbopack resolveAlias. User next.config stays clean.

4. CI typecheck

Scaffold "postinstall": "arkenv generate" so tsc --noEmit succeeds before next build runs the plugin.

5. Security guardrail

Generated server artifacts inject "server-only"; so accidental Client Component imports fail the App Router build.

6. No generated server factory (ADR 0016)

Server DX follows #1403 (Nuxt-style auto-extend on @arkenv/nextjs/server), not arkenv/gen/server / server.gen.ts. Client virtual factory remains for Next’s runtimeEnv tax only.

Vision (v1) — directional

User-facing tree (target)

env/
  client.ts
  server.ts   # exposeToClient: [...] tags client-safe keys (shared file phased out)

Machine-owned client factory

// env/client.ts
import arkenv from "arkenv/gen/client"; // exact specifier TBD — see open questions

Desired properties:

Relationship to #1307 / #1403

#1307 / #1401 (Nuxt) #1403 (Next auto-extend) This RFC (client factory placement)
Mechanism Virtual alias → user’s env/client.ts Same pattern on Next Virtual import → generated client factory in .arkenv/
What it solves Manual extends Manual extends runtimeEnv codegen out of env/
Blocks the other? No No No

Relationship to #1304

#1304 closed as superseded for end-state (in-tree server factory). This RFC + #1403 are the preferred v1 shape.

Open questions (still ready for human)

  1. Specifier namespace — lean package-style (arkenv/gen/client or @arkenv/nextjs/gen/client); exact string deferred to implementation PR. Optional cross-host rename with Nuxt #arkenv/… later.
  2. Flat layout — same virtual surface, or keep a simpler single-module story?
  3. Migration — soft compat for ./generated/env.gen imports in v1, or hard cut?
  4. exposeToClient ↔ strict transition — how fully it replaces internal/shared / Make env/internal/shared.ts optional in strict layout (dev/v0) #1448; docs & scaffold mapping.

On-disk locationlocked: root .arkenv/ (gitignore + regenerate).
Generated server factory / cross-host gen/serverout: #1403 + ADR 0016.

Non-goals (for this RFC)

Success sketch

  • Strict env/ is user-authored only (no sibling *.gen.ts)
  • Generated client factory lives in .arkenv/, reached only via virtual import
  • #1109 remains intact (cache-only + virtual import, not hidden app modules)
  • Next strict server needs no manual extends (via (v1) Auto-extend client env in Next.js strict layout via virtual module alias #1403)
  • Docs/CLI scaffolds teach virtual imports, .gitignore for .arkenv/, and postinstall generate

Metadata

Metadata

Assignees

No one assigned

    Labels

    @arkenv/nextjsIssues or Pull Requests involving the Next.js integration for ArkEnvenhancementNew feature or improvementready for humanNeeds a judgment call or design decision before implementationrfcA proposal open for discussion, inviting feedback before writing code. May or may not land as-is

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions