0011 — Internalize is a reference, not an architectural exemplar
- Status: accepted
- Date: 2026-05-06
- Deciders: Derek
Context
Ark is the multi-tenant productized successor to internalize, a single-tenant operations platform that has run for an art collective of ~30 members for several months. Internalize works — feature-wise it’s the most mature artifact in the ecosystem, and the patterns it demonstrates (forum + mentions + permissions + realtime + push notifications) are what make ark’s pitch credible.
But internalize was built under POC pressure for a single, known tenant. Direct framing from the project owner: “internalize repo is a borderline-vibe proof of concept. it’s working with a 30 mem collective but it’s definitely not fully vetted.”
If we lift internalize code 1:1 into ark, we import POC compromises into a system whose entire value proposition is stability for one engineer running N tenants (ADR 0009). The “one engineer can sustain N tenants” math collapses if the codebase contains the kind of carefully-load-bearing-but-undocumented code that POCs accumulate.
Decision
Treat internalize as a domain reference and a feature spec, not as the code we ship. When porting any internalize feature into ark, the work is a re-implementation, not a copy-paste-and-edit.
The lift discipline, applied to every module:
- Read internalize first to learn the domain — what does the feature do, what data shape is right, what gotchas exist (often documented inline in
CLAUDE.md“Common Gotchas” sections). - Write the domain understanding down in 1-2 paragraphs in the new ark package’s
CLAUDE.md. This is the lift artifact. - Write tests first for the ark version, based on the domain understanding (per ADR 0009).
- Implement the ark version from those tests, in the ark style — strict TS, Zod boundaries, packages with clear contracts, no magic.
- Code-review pass before merging. Use the
simplifyskill or request explicit review for any module touching auth, RLS, permissions, or notifications.
The ark version stands on its own. Internalize is referenced in commit messages and CLAUDE.md notes as the source of domain understanding, but the implementation is ours.
Operating principles for the port
These are not new — they’re the standard “good engineering” principles. Calling them out explicitly because the temptation to skip them when “the original works” is real.
- No magic. Everything explicit: imports, types, parameters, middleware ordering, role names. No ambient state, no implicit conventions.
- DRY at the right altitude. Don’t share types between unrelated domains. Don’t share once; share when a pattern emerges twice. Premature abstraction is worse than a little duplication.
- KISS. A function over ~30 lines or doing more than one thing gets split. A route handler reaching across three concerns means those concerns become packages.
- Type-safe boundaries. Every request, response, DB row, env var, config file goes through a Zod schema. Never trust unvalidated data crossing a boundary.
- No
any, no// @ts-nocheck, no broadtry/catchswallows. If you reach for one of these, stop and find the right abstraction. - Observable side-effects. Internalize does fire-and-forget notification dispatch (correct in spirit). Ark does fire-and-forget with structured logging — every async dispatch logs success/failure with correlation ids.
- Tests as executable spec. If a behavior isn’t in a test, it’s not enforced. Especially for permissions, RLS, and any code path that touches multiple tenants.
Anti-patterns from internalize specifically NOT to carry forward
These are observations from the source-map review, not criticisms — internalize’s tradeoffs were appropriate for its scope. They’re inappropriate for ark’s scope.
- Inline SQL strings in route handlers
- Plain JS without types at request/response boundaries
- Mixed concerns in single files (a route handler that also formats email and writes audit logs)
- Hardcoded org name / domain / branding (already addressed by ADR 0002 / 0004)
- Permission checks duplicated across routes (centralize in middleware)
- Ad-hoc error shapes (use
@ark/coreerror classes) - Silent fire-and-forget without structured logging
- 1700-LOC single-route files
- Rule names hardcoded in middleware that should be in the DB
Consequences
Easier:
- Ark stays maintainable as it grows. Each module has clean contracts; tests document behavior.
- AI agents working on ark have a clear model — every module looks like every other module.
- “One engineer + agents at N tenants” remains believable because the codebase doesn’t accumulate hidden complexity.
Harder:
- Porting is slower than copy-paste. We pay the cost up front in exchange for not paying it later in bugs.
- We have to resist the temptation to “just lift this one thing” without going through the discipline.
- The first port (the auth keystone, Phase 1.5a) sets the template for all subsequent ports — we have to do it well.
Trip-wires
We revisit this stance only if:
- The pace of feature delivery becomes an existential problem before any tenant onboards. (At that point we’d write a new ADR explaining what to relax.)
- A tenant emerges with a hard deadline that requires shipping a known-but-unvetted feature; even then, the relaxation is scoped to that feature with that tenant, not a general policy change.
Alternatives considered
- 1:1 port (copy-paste + edit). Fastest in the short term. Imports POC structure into the productized codebase. Rejected — directly contradicts ADR 0009.
- Greenfield (don’t reference internalize at all). Loses the hard-won domain knowledge. The user has spent months learning what works for a real co-op; throwing that away is wasteful. Rejected.
- Half-and-half: lift code but rewrite the bits that smell. Tempting and probably what would happen organically. Hard to draw the line consistently — the rewrite-everything discipline is easier to maintain than a moving “smells bad” judgment.