From Enterprise Boilerplate to Atlas: Patterns That Survived
How typed environment variables, provider composition, shared UI primitives, and secret scanning evolved from a Next.js starter into enforceable Atlas conventions.
- Published
- January 16, 2026
- Updated
- Updated July 12, 2026
- Author
- Daniel Mark
- Reading time
- 10 min read
Most frontend platforms do not begin as platforms. They begin as one application with a few decisions that worked well enough to repeat.
The enterprise frontend boilerplate I worked on at AxLabs GmbH was that starting point. It gave new Next.js applications a common foundation for environment validation, provider composition, feedback states, and secret scanning. It was intentionally starter-sized: clone it, replace the product-specific pieces, and begin shipping.
That model works until several applications inherit the same conventions and start changing them independently. A typed environment module becomes optional. Providers accumulate at the root. Shared components drift between repositories. Documentation describes an intended architecture that the code no longer enforces.
Atlas was my response to that problem. It was not a folder-for-folder migration of the boilerplate. It was an attempt to identify which decisions deserved to become platform contracts, which pieces belonged in shared packages, and which starter conventions should remain application-specific.
The useful progression was not boilerplate to larger boilerplate. It was convention to enforcement.
Concern | Boilerplate | Atlas |
|---|---|---|
Environment variables | Typed Zod schemas | Config facade plus lint enforcement |
Provider composition | One application-wide stack | Providers scoped to the routes that need them |
Feedback components | Application-local files | Shared primitives in |
Secret scanning | Husky and CI workflows | The same executable pattern, retained |
Architecture decisions | Documented expectations | ADRs backed by package and lint boundaries |
The problem with successful starters
A starter repository solves the blank-page problem. It gives a team a working answer to questions that otherwise appear during the first week of every project:
- How are environment variables validated?
- Where are global providers mounted?
- How are loading and error states presented?
- How are secrets detected before they reach the repository?
- Which utilities should every application use?
The danger appears when the starter succeeds.
Once several products are created from it, every repository owns a disconnected copy. Fixing a pattern in the starter does not fix existing applications. Updating documentation does not stop a feature from bypassing the documented path. Shared components can carry the same name while behaving differently.
Copying code distributes an implementation once. It does not distribute governance.
Atlas therefore needed to preserve the useful decisions while changing how those decisions were maintained.
Environment validation became an enforced boundary
The boilerplate used @t3-oss/env-nextjs with Zod schemas split between public and server-only configuration:
This provided an important baseline. Invalid or missing configuration, such as a malformed JWT_SECRET, could fail during the build or application startup instead of surfacing later inside a request.
It also prevented server-only values from being treated casually as public configuration.
The limitation was not the library. The limitation was that a developer could still bypass the validated module and read from process.env directly inside a feature.
Documentation could say “always use the environment module,” but that remained a request rather than an architectural boundary.
Atlas retained the typed-schema approach and placed a config facade in front of environment access. Application code consumes validated configuration through that facade. ESLint rejects direct process.env access outside the approved configuration modules.
Conceptually, the boundary looks like this:
The important change is not syntactic. Both approaches can return the same string.
The difference is that the second approach gives the platform one place to validate, normalize, document, and test configuration. It also gives CI a way to stop new code from silently bypassing those guarantees.
A starter can recommend a configuration path. A platform must make the supported path easier to follow and the unsupported path difficult to merge.
The provider tree became route-scoped
The boilerplate composed its application providers through a single MainProvider. Environment state, React Query, theme state, and toast feedback were nested into one stack mounted from the root layout.
In simplified form, the structure looked like this:
That is a sensible design for a single-purpose application. It creates one place to understand global client state and avoids scattering providers across unrelated components.
The assumption stopped holding when Atlas needed to support both static marketing routes and application routes.
A marketing page does not necessarily need a client-side query cache. Mounting application-only providers at the root would make them part of every route, including routes designed to remain mostly server-rendered.
Atlas kept provider composition but changed its scope. DataProviderLayout wraps the (app) and /demo route groups rather than the marketing root.
The resulting boundary is closer to this:
This does not mean every provider must be pushed as far down the tree as possible. Excessive provider fragmentation can make ownership harder to follow.
The rule is more practical: mount a provider at the highest boundary shared by the routes that genuinely need it.
For the boilerplate, that boundary was the root. For Atlas, it was the application route group. The pattern survived, but the scope changed with the architecture.
Feedback components became shared UI primitives
The boilerplate included feedback components such as error-fallback.tsx. These established a visual language for failures and other non-happy-path states.
Their presence also exposed a distinction that starter repositories often blur: a component existing in the repository is not the same as a capability being integrated into the application.
A reusable error fallback only helps when it is connected to the appropriate App Router error.tsx or global-error.tsx boundary. A loading component only becomes a convention when applications use it consistently. A dependency only becomes a supported capability when it has configuration, examples, tests, and a place in CI.
Atlas moved the reusable parts of this feedback language into @atlas/ui and documented them through Storybook. That changed their role.
They were no longer files copied into whichever application needed them. They became shared primitives with a visible API and a consistent implementation.
This extraction was worthwhile because the components represented cross-application behavior. Product-specific error messages still belong to the product. The visual structure, accessibility behavior, and common states can belong to the platform.
The distinction matters. A shared package should not absorb every component that appears twice. It should own components whose consistency is more valuable than the freedom to let each application reinvent them.
Secret scanning survived almost unchanged
Not every pattern required architectural reinvention.
The boilerplate used a Gitleaks pre-commit hook through Husky:
The scan ran through a pinned Docker image before lint-staged. CI complemented it with a repository-wide scan, while Docker’s --network=none option prevented the scanner container from making network requests during execution.
Atlas retained the same dual-layer approach.
The staged pre-commit scan provides fast local feedback. The CI scan acts as the repository-level backstop. Developers can bypass local hooks, machines can be misconfigured, and historical content needs broader coverage than a staged-file check provides.
This pattern transferred cleanly because it was already executable and self-contained. It did not depend on developers remembering a convention or copying a component correctly.
I covered the Docker requirement, pinned scanner version, staged redaction, and CI workflow in more detail in Secret Scanning in a Frontend Repo: Gitleaks in Husky and CI.
What I deliberately left behind
Building Atlas was as much about rejecting starter assumptions as preserving useful code.
One provider stack for every route
A universal MainProvider remains appropriate for applications where nearly every route shares the same client-side requirements. It is less appropriate for a platform containing static marketing pages, authenticated application routes, and isolated demonstrations.
Atlas retained provider composition without treating the root layout as the automatic home for every provider.
Documentation-only conventions
README guidance is useful for explaining why a decision exists. It is weak as the only mechanism protecting that decision.
If direct environment access is unsupported, lint should reject it. If packages have intended dependency directions, tooling should validate them. If an architectural rule matters during review, CI should verify as much of it as reasonably possible.
An ADR records the decision. Enforcement keeps the implementation from quietly contradicting it.
Copy-pasted shared components
Copying a feedback component is faster than designing a shared package API. It also creates another implementation that must be updated independently.
Atlas moved stable, cross-application primitives into @atlas/ui. Product-specific views remained inside their applications.
The goal was not to maximize the shared package. It was to give genuinely shared behavior one maintained source.
Dependencies presented as finished capabilities
A package appearing in devDependencies does not mean the repository has integrated that tool.
Playwright, for example, becomes a project capability when the repository includes configuration, meaningful end-to-end tests, documented commands, and CI execution. Installing the package is only the first step.
This became a broader platform rule: describe the behavior the repository can execute, not the tooling it appears prepared to use someday.
Forking as the long-term distribution model
Forking or copying a starter is effective at project creation time. It is a poor mechanism for distributing later architectural improvements.
Atlas uses packages, lint rules, layouts, and ADRs to keep shared decisions visible after the first commit. Some improvements can therefore reach multiple applications without manually comparing each repository against the original starter.
Not every decision can or should be centralized, but critical conventions should not depend on archaeological work across old forks.
Why GitMyABI remained a product application
GitMyABI provides a useful contrast.
Its frontend is organized around product concerns: ABI extraction, build history, teams, billing, documentation, and authenticated workflows. A product-scale MainProvider is reasonable there because most of the surface belongs to the application.
Atlas has a different responsibility. It organizes cross-cutting concerns that can support several products without knowing their domains.
Both share the same underlying instincts: validate configuration, compose providers intentionally, use typed data boundaries, and provide consistent feedback. Only Atlas turns those instincts into reusable packages and enforced platform rules.
That distinction prevents a common mistake: treating every well-structured product repository as a platform.
A platform is not merely an application with more folders. It owns stable contracts consumed by applications with different domain requirements.
The tradeoffs of turning conventions into contracts
The move from starter to platform was not free.
Lint enforcement creates friction when a developer needs an exception. A config facade introduces another abstraction over values that could technically be read directly. Shared packages require more careful API decisions than local components. Route-scoped providers create additional layout boundaries to understand.
Those costs are justified only when the underlying problem repeats.
If one application needs a component, local ownership is usually simpler. If several applications need the same behavior and inconsistency creates maintenance risk, extraction becomes more valuable.
The same applies to architectural governance. ADRs and lint rules should protect consequential decisions, not memorialize every preference. Too many rules turn a platform into an obstacle course. Too few rules reduce it to another starter repository with ambitious documentation.
The platform has to earn its abstractions.
What I would improve next
The next step is to make the relationship between documented capabilities and executable capabilities even harder to misrepresent.
That means:
- Ensuring every claimed tool has configuration, examples, and CI coverage.
- Testing package boundaries as part of the normal pipeline.
- Keeping setup documentation synchronized with validated environment schemas.
- Recording exceptions when an application must bypass a platform convention.
- Reviewing ADRs as the platform evolves rather than treating accepted decisions as permanent scripture.
The goal is not perfect uniformity. It is making deliberate divergence visible.
Closing
The most valuable parts of the enterprise boilerplate were not its folder names or component locations. They were the decisions underneath them: validate configuration early, compose global state intentionally, standardize feedback, and scan for secrets before they reach the repository.
Atlas preserved those decisions while changing how they were distributed and enforced.
Typed environment schemas became a guarded config boundary. A root provider stack became route-scoped composition. Local feedback components became shared UI primitives. Gitleaks remained largely unchanged because it was already an executable workflow rather than a documented convention.
That is the migration pattern worth repeating.
Copy the decisions that survived real application work. Extract the behavior that must remain consistent. Enforce the boundaries that matter.
Do not copy the folder tree.
Written by
Daniel Mark
Senior Frontend Engineer
Daniel Mark is a senior frontend engineer and product consultant with 7+ years of experience building resilient product interfaces for Web3, SaaS, and enterprise teams. He writes about frontend architecture, design systems, performance, SEO, and the engineering decisions that turn complex systems into dependable user experiences.