Skip to content
Merged
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
29 changes: 0 additions & 29 deletions .env.example

This file was deleted.

102 changes: 102 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# This env file uses @env-spec - see https://varlock.dev/env-spec for more info
#
# @defaultRequired=true @defaultSensitive=false
# @currentEnv=$NODE_ENV
# @generateTypes(lang=ts, path=types/env-vars.d.ts)
# ----------

# @type=enum(development, production, test)
NODE_ENV=development
# @type=enum(development, production, test)
MODE=$NODE_ENV

# @type=port
PORT=3000

LITEFS_DIR="/litefs/data"
DATABASE_PATH="./prisma/data.db"
DATABASE_URL="file:./data.db?connection_limit=1"
CACHE_DATABASE_PATH="./other/cache.db"

# used to secure sessions
# @sensitive
# @docs(https://stack-staging.epicweb.dev/topic/deployment)
SESSION_SECRET="super-duper-s3cret"

# encryption seed for honeypot server
# @sensitive
# @docs(https://stack-staging.epicweb.dev/topic/deployment)
HONEYPOT_SECRET="super-duper-s3cret"

# this is set to a random value in the Dockerfile
# @sensitive
INTERNAL_COMMAND_TOKEN="some-made-up-token"

# set to false to prevent search engines from indexing the website (defaults to allow)
ALLOW_INDEXING=true

# enables mocks for external services
MOCKS=forEnv(development, test)

# will be set to curent commit sha in deployments
# @optional
COMMIT_SHA=

# API key for Resend (email service)
# @type=string(startsWith=re_)
# @sensitive
# @optional # remove this if using resend
# @docs(https://resend.com/docs/dashboard/api-keys/introduction#what-is-an-api-key)
RESEND_API_KEY=

# will be set to true when running in CI
CI=false

# Sentry settings (error tracking)
# note that SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT are optional
# but enable @sentry/react-router integration and release tagging
# ---
# @type=url
# @optional # remove this if using sentry
# @example=https://examplePublicKey@o0.ingest.sentry.io/0
# @docs(https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
SENTRY_DSN=
# @optional @sensitive
SENTRY_AUTH_TOKEN=
# @required=if($SENTRY_AUTH_TOKEN)
SENTRY_ORG=
# @required=if($SENTRY_AUTH_TOKEN)
SENTRY_PROJECT=

# GitHub settings
#
# the mocks and some code rely on these being prefixed with "MOCK_"
# if they aren't then the real github api will be attempted
# ---
GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID"
# @sensitive
GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET"
# @sensitive
GITHUB_TOKEN="MOCK_GITHUB_TOKEN"
# @type=url
GITHUB_REDIRECT_URI="https://example.com/auth/github/callback"


# Tigris Object Storage (S3-compatible) Configuration
# ---
AWS_ACCESS_KEY_ID="mock-access-key"
# @sensitive
AWS_SECRET_ACCESS_KEY="mock-secret-key"
AWS_REGION="auto"
# @type=url
AWS_ENDPOINT_URL_S3="https://fly.storage.tigris.dev"
BUCKET_NAME="mock-bucket"

# Populated by fly.io
# ---
# current fly.io region
# @optional
FLY_REGION=
# app name as set in fly.io
# @optional
FLY_APP_NAME=
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DATABASE_PATH="./tests/prisma/base.db"
DATABASE_URL=file:../tests/prisma/base.db
12 changes: 0 additions & 12 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ jobs:
- name: 📥 Download deps
uses: bahmutov/npm-install@v1

- name: 🏄 Copy test env vars
run: cp .env.example .env

- name: 🛠 Setup Database
run: npx prisma migrate deploy && npx prisma generate --sql

Expand All @@ -57,9 +54,6 @@ jobs:
- name: 🏗 Build
run: npm run build

- name: 🏄 Copy test env vars
run: cp .env.example .env

- name: 🛠 Setup Database
run: npx prisma migrate deploy && npx prisma generate --sql

Expand All @@ -81,9 +75,6 @@ jobs:
- name: 📥 Download deps
uses: bahmutov/npm-install@v1

- name: 🏄 Copy test env vars
run: cp .env.example .env

- name: 🛠 Setup Database
run: npx prisma migrate deploy && npx prisma generate --sql

Expand All @@ -98,9 +89,6 @@ jobs:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4

- name: 🏄 Copy test env vars
run: cp .env.example .env

- name: ⎔ Setup node
uses: actions/setup-node@v4
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ node_modules
/build
/server-build
.env
.env.local
.env.*.local
.cache

/prisma/data.db
Expand All @@ -26,3 +28,4 @@ node_modules
# generated files
/app/components/ui/icons
.react-router/
/types/env-vars.d.ts
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ node_modules
/public/build
/server-build
.env
.env.*

/test-results/
/playwright-report/
/playwright/.cache/
/tests/fixtures/email/*.json
/coverage
/prisma/migrations
/types/env-vars.d.ts

package-lock.json
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"prisma.prisma",
"qwtel.sqlite-viewer",
"yoavbls.pretty-ts-errors",
"github.vscode-github-actions"
"github.vscode-github-actions",
"varlock.env-spec-language"
]
}
69 changes: 25 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,35 @@
<div align="center">
<h1 align="center"><a href="https://www.epicweb.dev/epic-stack">The Epic Stack 🚀</a></h1>
<strong align="center">
Ditch analysis paralysis and start shipping Epic Web apps.
</strong>
<p>
This is an opinionated project starter and reference that allows teams to
ship their ideas to production faster and on a more stable foundation based
on the experience of <a href="https://kentcdodds.com">Kent C. Dodds</a> and
<a href="https://github.com/epicweb-dev/epic-stack/graphs/contributors">contributors</a>.
</p>
</div>
# Epic Stack + Varlock

```sh
npx epicli
```
An example repo using [Varlock](https://varlock.dev/) within the Epic Stack to help manage configuration and secrets.

[![The Epic Stack](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/246885449-1b00286c-aa3d-44b2-9ef2-04f694eb3592.png)](https://www.epicweb.dev/epic-stack)
With Varlock, we convert the `.env.example` file into a `.env.schema` which contains additional schema information about all configuration in the system. This will improve developer onboarding into the epic stack, as well as ongoing DX as devs add more config into their apps. It adds additional guardrails around configuration in general, and notably adds additional protection for sensitive secrets.

[The Epic Stack](https://www.epicweb.dev/epic-stack)
## Why do this?
- validations, default values, and documentation are all now in one source of truth (`.env.schema`)
- no more duplication between `.env.example` and `.env`, which means it will never get out of sync
- only overrides must be added by user
- clear env validation, decoupled from the application booting
- improved TS types / IntelliSense
- allows more flexible validation and composition of values based on other items
- easy to now pull secrets from secure backends like 1pass, etc
- leak prevention! log redaction!
- clear error messages when accessing bad env vars, or using them in wrong place

<hr />
## Screenshots

## Watch Kent's Introduction to The Epic Stack
Some screenshots of varlock in action:

[![Epic Stack Talk slide showing Flynn Rider with knives, the text "I've been around and I've got opinions" and Kent speaking in the corner](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/277818553-47158e68-4efc-43ae-a477-9d1670d4217d.png)](https://www.epicweb.dev/talks/the-epic-stack)
_`varlock load` showing loaded and validated env_
<img width="488" height="393" alt="image" src="https://github.com/user-attachments/assets/9e80775e-ddf4-47b8-8ca1-0e4471c37299" />

["The Epic Stack" by Kent C. Dodds](https://www.epicweb.dev/talks/the-epic-stack)
_Improved IntelliSense_
<img width="435" height="131" alt="image" src="https://github.com/user-attachments/assets/3732dc0f-79f5-4ee5-a846-d314b31db1da" />

## Docs
_Leak detection example_
<img width="657" height="201" alt="image" src="https://github.com/user-attachments/assets/7598448a-d18c-47b6-b7c5-df3c68bbd875" />

[Read the docs](https://github.com/epicweb-dev/epic-stack/blob/main/docs)
(please 🙏).
_Log redaction example_
<img width="202" height="52" alt="image" src="https://github.com/user-attachments/assets/3643b5d0-eec6-4f68-a488-0dfda7f18684" />

## Support

- 🆘 Join the
[discussion on GitHub](https://github.com/epicweb-dev/epic-stack/discussions)
and the [KCD Community on Discord](https://kcd.im/discord).
- 💡 Create an
[idea discussion](https://github.com/epicweb-dev/epic-stack/discussions/new?category=ideas)
for suggestions.
- 🐛 Open a [GitHub issue](https://github.com/epicweb-dev/epic-stack/issues) to
report a bug.

## Branding

Want to talk about the Epic Stack in a blog post or talk? Great! Here are some
assets you can use in your material:
[EpicWeb.dev/brand](https://epicweb.dev/brand)

## Thanks

You rock 🪨
_Example of failing env validation_
<img width="430" height="176" alt="image" src="https://github.com/user-attachments/assets/d6258c48-43b8-4b6a-95d9-596a99f24e2b" />
1 change: 1 addition & 0 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { HydratedRouter } from 'react-router/dom'
import { ENV } from 'varlock/env'

if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
void import('./utils/monitoring.client.tsx').then(({ init }) => init())
Expand Down
20 changes: 8 additions & 12 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,26 @@ import {
type ActionFunctionArgs,
type HandleDocumentRequestFunction,
} from 'react-router'
import { getEnv, init } from './utils/env.server.ts'
import { ENV } from 'varlock/env'
import { getInstanceInfo } from './utils/litefs.server.ts'
import { NonceProvider } from './utils/nonce-provider.ts'
import { makeTimings } from './utils/timing.server.ts'

export const streamTimeout = 5000

init()
global.ENV = getEnv()

const MODE = process.env.NODE_ENV ?? 'development'

type DocRequestArgs = Parameters<HandleDocumentRequestFunction>

export default async function handleRequest(...args: DocRequestArgs) {
const [request, responseStatusCode, responseHeaders, reactRouterContext] =
args
const { currentInstance, primaryInstance } = await getInstanceInfo()
responseHeaders.set('fly-region', process.env.FLY_REGION ?? 'unknown')
responseHeaders.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
responseHeaders.set('fly-region', ENV.FLY_REGION ?? 'unknown')
responseHeaders.set('fly-app', ENV.FLY_APP_NAME ?? 'unknown')
responseHeaders.set('fly-primary-instance', primaryInstance)
responseHeaders.set('fly-instance', currentInstance)

if (process.env.NODE_ENV === 'production' && process.env.SENTRY_DSN) {
if (ENV.NODE_ENV === 'production' && ENV.SENTRY_DSN) {
responseHeaders.append('Document-Policy', 'js-profiling')
}

Expand Down Expand Up @@ -72,8 +68,8 @@ export default async function handleRequest(...args: DocRequestArgs) {
directives: {
fetch: {
'connect-src': [
MODE === 'development' ? 'ws:' : undefined,
process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
ENV.MODE === 'development' ? 'ws:' : undefined,
ENV.SENTRY_DSN ? '*.sentry.io' : undefined,
"'self'",
],
'font-src': ["'self'"],
Expand Down Expand Up @@ -114,8 +110,8 @@ export default async function handleRequest(...args: DocRequestArgs) {

export async function handleDataRequest(response: Response) {
const { currentInstance, primaryInstance } = await getInstanceInfo()
response.headers.set('fly-region', process.env.FLY_REGION ?? 'unknown')
response.headers.set('fly-app', process.env.FLY_APP_NAME ?? 'unknown')
response.headers.set('fly-region', ENV.FLY_REGION ?? 'unknown')
response.headers.set('fly-app', ENV.FLY_APP_NAME ?? 'unknown')
response.headers.set('fly-primary-instance', primaryInstance)
response.headers.set('fly-instance', currentInstance)

Expand Down
Loading