Reusable Next.js foundation for Open Elements applications: OIDC auth (with refresh-token handling), the backend proxy, the auth middleware, admin pages (audit logs, users, server status, bearer token, API keys, webhooks), the login page, the forbidden page, the OE root layout, and the supporting providers and types.
The package is consumed by Open CRM and is intended for the wider Open Elements app family.
pnpm add @open-elements/nextjs-app-layerPeer dependencies (must be provided by the consuming app):
pnpm add @open-elements/ui next next-auth react react-dom lucide-reactROLE_ADMIN,ROLE_IT_ADMIN,hasRole(session, role)ForbiddenError- DTO types:
Page<T>,UserDto,AuditAction,AuditLogDto,ApiKeyDto,ApiKeyCreateDto,ApiKeyCreatedDto,WebhookDto,WebhookCreateDto,WebhookUpdateDto,TranslationConfigDto,PageRequest AppLayerTranslationProvider,useAppLayerTranslations,appLayerTranslations, typeAppLayerTranslationsSessionProvider,ForbiddenPage,BearerTokenCard,AddCommentDialogApiClientProvider,useApiClient,defaultApiClient, typeAppLayerApiClient- Page factories + clients + metas for each admin page:
createAuditLogsPage,AuditLogsClient,auditLogsPageMetacreateUsersPage,UsersClient,usersPageMetacreateServerStatusPage,ServerStatusClient,serverStatusPageMetacreateBearerTokenPage,BearerTokenClient,bearerTokenPageMetacreateApiKeysPage,ApiKeysClient,apiKeysPageMetacreateWebhooksPage,WebhooksClient,webhooksPageMetacreateLoginPage,LoginClient
createAppLayerAuth({ issuer, clientId, clientSecret })createBackendProxyHandler({ backendUrl, auth })createLogoutHandler({ auth, oidcIssuer, authUrl })middlewareConfig(reference value — see warning below)
Side-effect module that augments NextAuth's Session type. Activate via:
import "@open-elements/nextjs-app-layer/server/next-auth-types";inside your own auth.ts.
OERootLayout — root layout component that renders <html> with the
Montserrat / Lato font variables and the full provider stack
(SessionProvider, LanguageProvider, AppLayerTranslationProvider,
ApiClientProvider). Kept on its own entry point so the next/font/google
runtime call does not get pulled into every consumer of the client barrel.
// src/auth.ts
import "@open-elements/nextjs-app-layer/server/next-auth-types";
import { createAppLayerAuth } from "@open-elements/nextjs-app-layer/server";
export const { handlers, auth, signIn, signOut, oidcIssuer } =
createAppLayerAuth({
issuer: process.env.OIDC_ISSUER_URI,
clientId: process.env.OIDC_CLIENT_ID,
clientSecret: process.env.OIDC_CLIENT_SECRET,
});// src/app/api/[...path]/route.ts
import { auth } from "@/auth";
import { createBackendProxyHandler } from "@open-elements/nextjs-app-layer/server";
const handler = createBackendProxyHandler({
backendUrl: process.env.BACKEND_URL ?? "http://localhost:8080",
auth,
});
export { handler as GET, handler as POST, handler as PUT, handler as DELETE };// src/app/api/logout/route.ts
import { auth, oidcIssuer } from "@/auth";
import { createLogoutHandler } from "@open-elements/nextjs-app-layer/server";
export const GET = createLogoutHandler({
auth,
oidcIssuer,
authUrl: process.env.AUTH_URL ?? "http://localhost:3000",
});// src/middleware.ts
export { auth as middleware } from "@/auth";
// `config` MUST be a static literal here. Next.js' build-time analyzer
// extracts the matcher directly from middleware.ts and does NOT follow
// re-exports across package boundaries. Re-exporting the lib's
// `middlewareConfig` as `config` silently disables the matcher in production
// — `/_next/static/*` requests get routed through the auth middleware and
// the deployment breaks. The lib's `middlewareConfig` is reference-only.
export const config = {
matcher: [
"/((?!api/auth|api/logout|login|_next/static|_next/image|favicon\\.ico|.*\\.svg$|.*\\.png$|.*\\.jpg$|.*\\.ico$).*)",
],
};// src/app/layout.tsx
import type { Metadata } from "next";
import { OERootLayout } from "@open-elements/nextjs-app-layer/layout";
import { translations } from "@/lib/i18n";
import "./globals.css";
export const metadata: Metadata = {
title: "My App",
description: "...",
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return <OERootLayout translations={translations}>{children}</OERootLayout>;
}// src/app/(app)/admin/audit-logs/page.tsx
import { auth } from "@/auth";
import { createAuditLogsPage } from "@open-elements/nextjs-app-layer";
export default createAuditLogsPage({ auth });The same 2-line shape applies to every other admin page.
- OIDC role names:
IT-ADMINandADMIN(hardcoded). - Proxy pattern: every backend call goes through
/api/...in the same origin as the Next.js app. - Fonts: Montserrat (heading), Lato (body).
- Brand: provided by
@open-elements/ui(@import "@open-elements/ui/styles/brand.css"in the app'sglobals.css).
The current design intentionally keeps the public surface narrow. The following are not supported today and will be addressed in future versions:
- Configurable role names (per-app role mapping).
- Auth-factory extensibility hooks (custom claims, additional providers, signIn validation).
- Page-level customization (sub-component exports, slot props).
- Per-string translation overrides.
- Node.js ≥ 22 — the exact version is pinned in
.nvmrc(Node 24). With nvm, runnvm install && nvm usein the project root to match it. - pnpm — pinned via the
packageManagerfield inpackage.json. Enable it through Corepack (bundled with Node):corepack enable. The correct pnpm version is then selected automatically.
pnpm installpnpm run build # compile TypeScript to dist/ (tsc -p tsconfig.build.json)pnpm run typecheck # type-check without emitting
pnpm run lint # ESLint over src/
pnpm run format:check # Prettier check (use `pnpm run format` to auto-fix)
pnpm run test # Vitest (use `pnpm run test:watch` while developing)These are the same checks the release workflow runs in CI before publishing.
Releases run through the tag-triggered GitHub Actions workflow
(.github/workflows/release.yml) — there is no
local publishing step.
- Bump
versioninpackage.jsonand commit. - Tag the commit
vX.Y.Z(the tag must matchpackage.json) and push the tag. - CI verifies the tag, builds, type-checks, lints, formats, and tests, then stages the package to npm via OIDC (trusted publishing — no token). The version is uploaded to a staging queue and does not go live yet.
- Approve the staged release with 2FA — on npmjs.com,
or via the CLI:
pnpm stage listto find the id, thenpnpm stage approve <stage-id>(pnpm stage reject <stage-id>discards it). - Publish the draft GitHub Release that the workflow created.
- A Trusted Publisher is configured for the package on npmjs.com (repository
OpenElementsLabs/nextjs-app-layer, workflowrelease.yml, allowed actionpnpm stage publish). - No
NPM_TOKENis required — authentication uses OIDC, and provenance attestations are generated automatically.