Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ jobs:

build:
runs-on: ubuntu-24.04
env:
# Secrets for the frontend build (envsafe validation runs under NODE_ENV=production).
# Locally these come from scripts/with-secrets.sh + 1Password; in CI we use
# GitHub Actions secrets directly and the wrapper's `none` fallback passes them through.
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
TEST_SECRET: ${{ secrets.TEST_SECRET }}
steps:
- name: 📥 Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
Expand All @@ -47,5 +53,9 @@ jobs:
uses: ./.github/actions/node-setup

- name: 🏗️ Build packages
# Secrets for the frontend's envsafe validation come from the job env above
# (workflow-level GitHub Actions secrets). `pnpm build` invokes the wrapper,
# which in CI auto-detects no provider CLI is present and falls through to
# plain exec — secrets are already in process.env.
run: pnpm build
shell: bash
9 changes: 9 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ env:
NODE_ENV: test
CI: true
E2E_BASE_URL: http://localhost:3000
# Secrets used by the frontend (envsafe) at build + start. Locally these come from
# scripts/with-secrets.sh + 1Password; in CI we use GitHub Actions secrets directly
# and the wrapper's `none` fallback passes them through.
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
TEST_SECRET: ${{ secrets.TEST_SECRET }}

jobs:
e2e:
Expand All @@ -39,6 +44,10 @@ jobs:
uses: ./.github/actions/node-setup

- name: 🏗️ Build apps
# Secrets for the frontend's envsafe validation come from the workflow env
# above. `pnpm build` invokes the wrapper, which in CI auto-detects no
# provider CLI is present and falls through to plain exec — secrets are
# already in process.env.
run: pnpm build
shell: bash

Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pnpm-lock.yaml
*.md
Dockerfile

# Config files prettier can't parse natively
*.toml

# Build outputs
.next/
dist/
Expand Down
52 changes: 45 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ A modern React monorepo showcasing Next.js 15, TypeScript, and best practices fo

### Prerequisites

- **Node.js**: Version specified in `package.json`
- **pnpm**: Version specified in `package.json`
- **Corepack**: Run `corepack enable` to manage package manager versions automatically
- **[mise](https://mise.jdx.dev/)**: Required — installs and pins `node` and `pnpm` with checksum-verified binaries. Install with `brew install mise` on macOS (see the [Tool Management guide](documentation/Tool%20management.md) for other platforms and the full rationale).
- **Docker**: For containerized development (optional but recommended)
- **1Password CLI**: For injecting secrets when running locally (see [Environment secrets](#environment-secrets) below)

`node` and `pnpm` are managed by `mise` — you do **not** install them separately. This is part of the project's [supply-chain security posture](https://infinum.com/handbook/frontend/node/security/overview) *(internal handbook)*; see [Tool Management](documentation/Tool%20management.md) for details.

### One-Command Setup

```bash
# Clone, install, and start development
git clone <repository-url>
cd JS-React-Example
pnpm install && pnpm dev
mise trust && mise install && pnpm install
pnpm dev
```

`mise trust` approves this repo's `mise.toml`, `mise install` downloads and checksum-verifies `node` and `pnpm` against [mise.lock](mise.lock), and `pnpm dev` starts the dev server with secrets injected by [scripts/with-secrets.sh](scripts/with-secrets.sh) from 1Password. See [Tool Management](documentation/Tool%20management.md) for the full setup and [Environment Variables](documentation/Environment%20variables.md) for the secrets pipeline.

**Access the applications:**

- **Frontend**: http://localhost:3000
Expand Down Expand Up @@ -129,17 +133,46 @@ API patterns and development guide: [API Development Guide](documentation/API%20

### Key Variables

- **`NEXTAUTH_SECRET`**: Authentication secret (auto-generated in dev)
- **`NEXTAUTH_SECRET`**: Authentication secret — injected at runtime via [scripts/with-secrets.sh](scripts/with-secrets.sh) + 1Password (see **Environment secrets** below)
- **`NEXTAUTH_URL`**: Application URL for OAuth callbacks
- **OAuth providers**: `GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_ID`, etc.

### Environment Files

- **`.env.local`**: Local development overrides
- **`.env.compose`**: Docker Compose environment
- **`.env.local`**: Local development overrides (non-secret)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote - can have secrets but without values. Should serve as templates with all possible env variables

- **`.env.compose`**: Docker Compose environment (non-secret)

**Complete setup**: [Environment Variables Guide](documentation/Environment%20variables.md)

**Adding a new env variable:**

- **Non-secret** (feature flag, public URL, port, etc.) — see [Scenario A](documentation/Environment%20variables.md#scenario-a-non-secret-variable). Edit `.env` + validator; usually two files.
- **Secret** (API key, auth secret, DB password, etc.) — see [Scenario B](documentation/Environment%20variables.md#scenario-b-secret-used-only-at-runtime) for runtime-only secrets, [Scenario C](documentation/Environment%20variables.md#scenario-c-secret-needed-at-build-time-too) if envsafe validates it during `next build`. Both include vault setup, the `.env.secret` manifest, compose forwarding, the server validator, and CI wiring.

The canonical reference with every touch point per scenario plus a quick-scan matrix is [Adding a new environment variable](documentation/Environment%20variables.md#adding-a-new-environment-variable).

### Environment secrets

The goal is to keep secrets out of the filesystem entirely — nothing resolved on disk, nothing leaked to unrelated shell commands, only available to the processes that need them. To achieve this, a small wrapper script — [scripts/with-secrets.sh](scripts/with-secrets.sh) — injects secrets into the process environment at task-run time by delegating to the configured secret-store CLI. This is part of the project's [supply-chain security posture](https://infinum.com/handbook/frontend/node/security/overview) *(internal handbook)* — see the [Tool Management guide](documentation/Tool%20management.md) for the broader rationale.

**Reference setup: 1Password CLI**

This project uses the 1Password CLI (`op`). Each app declares its secret references in [apps/frontend/.env.secret](apps/frontend/.env.secret) using `op://vault/item/field` syntax — the file is committed because it contains *pointers*, not values. When `pnpm dev` runs, the wrapper invokes `op run --env-file=.env.secret -- next dev` and `op` resolves each reference against your authenticated 1Password session.

To set up your local environment:

1. Install the CLI: `brew install 1password-cli`
2. In the 1Password app, open **Settings → Developer** and enable **Integrate with 1Password CLI**.
3. Ensure your account has access to the vault where the project secrets are stored.

On first `pnpm dev`, 1Password prompts for authorization. After that, secrets are injected transparently — the example app renders "Hello Infinum!" on the login page when `TEST_SECRET` is available.

**Other backends**

1Password is the reference for this project, but the wrapper is the single integration point. Any other secret store with a `run`-style CLI (Doppler, Infisical, HashiCorp Vault, AWS Secrets Manager via `chamber`, Bitwarden, etc.) can be wired in by appending a `case` arm to [scripts/with-secrets.sh](scripts/with-secrets.sh) and using a corresponding manifest filename. Nothing else in the repo names a provider.

**Full details**: [Environment Variables Guide — Secrets](documentation/Environment%20variables.md#secrets)

## Development Workflow

### Daily Development
Expand Down Expand Up @@ -232,13 +265,18 @@ New UI components should:
### Available Guides

- [Monorepo Structure](documentation/Monorepo%20Structure.md) - Project organization and architecture
- [Tool Management (mise)](documentation/Tool%20management.md) - Why mise, how to install it, how tool pinning works
- [Development Workflow](documentation/Development%20Workflow%20Guide.md) - Git workflow, code review, releases
- [UI Components](documentation/UI%20Components%20Guide.md) - ShadCN component generation and customization
- [Semantic Tokens](documentation/Semantic%20Tokens%20Guide.md) - Design tokens and theming system
- [Internationalization](documentation/Internationalization%20Guide.md) - next-intl setup and configuration
- [Environment Variables](documentation/Environment%20variables.md) - Complete environment setup
- [Docker Setup](documentation/Docker%20setup.md) - Containerized development and deployment

### External references

- [Infinum handbook — Node.js Security](https://infinum.com/handbook/frontend/node/security/overview) *(internal)* - The security rationale behind `mise`, pnpm hardening, and AI-assisted development guardrails used in this repo.

### Architecture Decisions

Technical decisions are documented in [Architecture Decision Records](documentation/decision-record/) following the ADR format for traceability and team alignment.
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export default defineConfig({
],
webServer: process.env.CI
? {
// The frontend's `start` script wraps `next start` with
// scripts/with-secrets.sh. In CI the wrapper auto-detects no provider
// CLI is present and falls through to plain exec — secrets are already
// in the job env (set from GitHub Actions secrets in
// .github/workflows/e2e.yml).
command: 'pnpm --filter @infinum/frontend start',
port: 3000,
reuseExistingServer: !process.env.CI,
Expand Down
4 changes: 3 additions & 1 deletion apps/frontend/.env
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# NextAuth config
NEXTAUTH_SECRET="aVlrr1mODOkPGrXTt6vN515S8Cry5fnpX5pYFoM/bws="
# NEXTAUTH_SECRET handled through .env.secret + scripts/with-secrets.sh (see documentation/Environment variables.md)
NEXTAUTH_URL="http://localhost:3000"

# API
API_BASE_URL="http://localhost:3000/api"

# Common
NODE_ENV="development"

# TEST_SECRET handled through .env.secret + scripts/with-secrets.sh (see documentation/Environment variables.md)
5 changes: 4 additions & 1 deletion apps/frontend/.env.compose
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# NextAuth config
NEXTAUTH_SECRET="aVlrr1mODOkPGrXTt6vN515S8Cry5fnpX5pYFoM/bws="
# NEXTAUTH_SECRET forwarded from the host process by docker compose's `environment:` block.
# Locally: pnpm docker:prod resolves it via scripts/with-secrets.sh + .env.secret.
# In CI: the pipeline's secret provider exports it before compose runs.
# See documentation/Environment variables.md.
NEXTAUTH_URL="http://localhost:3000"

# API
Expand Down
12 changes: 12 additions & 0 deletions apps/frontend/.env.secret
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Secret references for this app. Resolved at task-run time by
# scripts/with-secrets.sh (which delegates to the configured provider CLI —
# 1Password by default, see documentation/Environment variables.md).
#
# The values here are POINTERS, not secrets. Reading this file does nothing
# without an authenticated session to the underlying secret store.
#
# Format: KEY=<provider-reference>
# 1Password: op://<vault>/<item>/<field>

NEXTAUTH_SECRET=op://JS general/ReactExample/NextAuthSecret
TEST_SECRET=op://JS general/ReactExample/TestSecret
3 changes: 3 additions & 0 deletions apps/frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ src/lib/i18n/locales/en/*.d.json.ts
.env*
!.env
!.env.compose
# Provider-reference manifest for scripts/with-secrets.sh — pointers only,
# never resolved values. See documentation/Environment variables.md.
!.env.secret

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see reason to do another type of env file.

.env and .env.compose should be templates (no commented out lines like you did), so they should even contain the secrets env, but with "REPLACE_ME" or "SECRET" values. On localhost you very often don't even need to use those secrets for 3rd party SaaS, and if you do, you can point your scripts to .env.local and .env.compose.local that are generated automatically on postinstall, are not commited, and can serve as the containers for the secret variables if needed

3 changes: 3 additions & 0 deletions apps/frontend/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ node_modules/
# Next specific
next-env.d.ts

# Config files prettier can't parse natively
*.toml

# Other
Dockerfile
24 changes: 22 additions & 2 deletions apps/frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
FROM node:24.11.0-alpine AS base
FROM node:24.15.0-alpine AS base
WORKDIR /app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# bash is needed by the wrapper shims under ./scripts (with-mise.sh,
# with-secrets.sh) that root + per-app pnpm scripts route through.
# Alpine ships only busybox sh, which lacks the bashisms ([[ ]], arrays).
RUN apk add --no-cache bash && corepack enable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is worth doing or there is a better way...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we completely get rid of corepack already and switch to mise in CI/CD also?

https://mise.jdx.dev/continuous-integration.html#github-actions


# # # TURBO PRUNE
FROM base AS turbo
Expand All @@ -21,6 +24,12 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM dependencies AS development

COPY --from=turbo /app/out/full ./
# Root `pnpm build` and the app's own `pnpm dev`/`pnpm build` route through
# ./scripts/with-mise.sh and ./scripts/with-secrets.sh respectively;
# scripts/ isn't part of `turbo prune --docker` output, so copy it
# explicitly. Both wrappers fall through to plain `exec` inside the
# container (no mise/op CLI to delegate to).
COPY scripts ./scripts

EXPOSE 3000

Expand All @@ -29,6 +38,17 @@ CMD ["pnpm", "dev"]
# # # Builder
FROM development AS builder

# Secrets needed for `next build` (envsafe validates them during static
# rendering). Passed in via docker-compose `build.args` from the host's
# env, which is populated by scripts/with-secrets.sh resolving op:// refs
# (locally) or the pipeline's env: block (CI). These ENV declarations are
# scoped to this stage — the production stage below restarts FROM base, so
# the values do not propagate into the final image.
ARG NEXTAUTH_SECRET
ARG TEST_SECRET
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
ENV TEST_SECRET=$TEST_SECRET
Comment thread
DarkoKukovec marked this conversation as resolved.

RUN pnpm build

# # # Production
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"build": "../../scripts/with-secrets.sh . -- next build",
Comment thread
DarkoKukovec marked this conversation as resolved.
"build:analyze": "ANALYZE=true pnpm build",
"check-licenses": "node ../../scripts/check-licenses-workspace.js",
"clean": "rm -rf node_modules .turbo .next .eslintcache",
"dev": "NODE_OPTIONS='--inspect' next dev",
"dev": "NODE_OPTIONS='--inspect' ../../scripts/with-secrets.sh . -- next dev",
"lint": "eslint . --cache",
"lint:fix": "eslint . --cache --fix",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"start": "next start",
"start": "../../scripts/with-secrets.sh . -- next start",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/src/app/[locale]/(public)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { secretEnv } from '../../../../lib/env/validate-env.server';
import { AuthCard } from '../_components/AuthCard/AuthCard';
import { LoginForm } from '../_components/LoginForm/LoginForm';

export default function LoginPage() {
const serverVars = secretEnv();
const secret = serverVars.TEST_SECRET;

return (
<AuthCard>
<LoginForm />
<code>{secret}</code>
</AuthCard>
);
}
5 changes: 5 additions & 0 deletions apps/frontend/src/lib/env/validate-env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ export const secretEnv = () =>
desc: 'A secure random string used by NextAuth for signing and encrypting JWT tokens and cookies. Required in production, especially when using JWT sessions.',
docs: 'https://next-auth.js.org/configuration/options#nextauth_secret',
}),
TEST_SECRET: str({
input: process.env.TEST_SECRET,
devDefault: 'Missing injected variable',
desc: 'Used to test if the mise env variables are injected correctly.',
}),
});
11 changes: 9 additions & 2 deletions apps/storybook/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
FROM node:24.11.0-alpine AS base
FROM node:24.15.0-alpine AS base

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're already having fun, make it 26.1.0 and get rid of corepack

WORKDIR /app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# bash is needed by the wrapper shims under ./scripts (with-mise.sh,
# with-secrets.sh) that root + per-app pnpm scripts route through.
# Alpine ships only busybox sh, which lacks the bashisms ([[ ]], arrays).
RUN apk add --no-cache bash && corepack enable

# # # TURBO PRUNE
FROM base AS turbo
Expand All @@ -22,6 +25,10 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM dependencies AS builder

COPY --from=turbo /app/out/full ./
# Root `pnpm build` runs ./scripts/with-mise.sh; scripts/ isn't part of
# `turbo prune --docker` output, so copy it explicitly. The wrapper falls
# through to plain `exec` inside the container (no mise to delegate to).
COPY scripts ./scripts

RUN pnpm build

Expand Down
14 changes: 14 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,24 @@ services:
context: ..
dockerfile: ./apps/frontend/Dockerfile
target: production
# Build-time secrets for envsafe validation during `next build`.
# Compose interpolates ${NAME} from the parent process's env — which
# the scripts/with-secrets.sh chain has already populated (locally
# via op, in CI via the pipeline's secret provider). Values flow:
# host env → compose interpolation → Dockerfile ARG → ENV in builder
# stage only (production stage starts FROM base, see Dockerfile).
args:
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
TEST_SECRET: ${TEST_SECRET}
ports:
- '3000:3000'
environment:
- HOSTNAME=0.0.0.0
# Secrets forwarded from the host process. These have no value here on purpose:
# compose passes through whatever is in the parent environment (populated locally
# by mise, or by the pipeline's secret provider in CI). Never inline real values.
- NEXTAUTH_SECRET
- TEST_SECRET
env_file:
- ../apps/frontend/.env
- ../apps/frontend/.env.compose
Expand Down
Loading
Loading