@kevinmarrec/create-app 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,3 +19,5 @@ bun create @kevinmarrec/app
19
19
  # OR
20
20
  bunx @kevinmarrec/create-app
21
21
  ```
22
+
23
+ After scaffolding, see the generated `README.md` in your project root for detailed setup instructions, including environment configuration, Docker setup, and development workflows.
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { x } from "tinyexec";
8
8
  import fs from "node:fs/promises";
9
9
 
10
10
  //#region package.json
11
- var version = "0.9.0";
11
+ var version = "0.10.1";
12
12
 
13
13
  //#endregion
14
14
  //#region src/utils/fs.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kevinmarrec/create-app",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.10.1",
5
5
  "packageManager": "bun@1.3.2",
6
6
  "description": "CLI that scaffolds an opinionated Bun & Vue fullstack application.",
7
7
  "author": "Kevin Marrec <kevin@marrec.io>",
@@ -65,6 +65,6 @@
65
65
  "tsdown": "^0.16.4",
66
66
  "typescript": "^5.9.3",
67
67
  "vitest": "^4.0.9",
68
- "vue-tsc": "^3.1.3"
68
+ "vue-tsc": "^3.1.4"
69
69
  }
70
70
  }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ CERTS_DIR="$(cd "$(dirname "$0")/../certs" && pwd)"
6
+
7
+ if ! command -v mkcert >/dev/null 2>&1; then
8
+ echo "Error: mkcert is not installed." >&2
9
+ echo "Please install mkcert before running this script." >&2
10
+ echo "See https://github.com/FiloSottile/mkcert#installation for installation instructions." >&2
11
+ exit 1
12
+ fi
13
+
14
+ mkdir -p "$CERTS_DIR"
15
+
16
+ mkcert -install 2> /dev/null
17
+
18
+ mkcert -cert-file "$CERTS_DIR/dev.localhost.crt" -key-file "$CERTS_DIR/dev.localhost.key" "dev.localhost" "*.dev.localhost"
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ CERTS_DIR="$(cd "$(dirname "$0")/../certs" && pwd)"
6
+
7
+ mkcert -uninstall 2> /dev/null || true
8
+
9
+ rm -f "$CERTS_DIR"/*.crt "$CERTS_DIR"/*.key 2> /dev/null || true
@@ -7,47 +7,23 @@ import { filesize } from 'filesize'
7
7
  import { x } from 'tinyexec'
8
8
  import { glob } from 'tinyglobby'
9
9
 
10
- interface FileStats {
11
- file: string
12
- size: number
13
- }
14
-
15
10
  async function getFileStats(directory: string) {
16
- const fileStats: FileStats[] = []
17
-
18
- for (const file of await glob(['**/*'], { cwd: directory })) {
19
- const size = fs.statSync(path.join(directory, file)).size
20
- fileStats.push({
11
+ const fileStats = await Promise.all(
12
+ (await glob(['**/*'], { cwd: directory })).map(async file => ({
21
13
  file,
22
- size,
23
- })
24
- }
14
+ size: fs.statSync(path.join(directory, file)).size,
15
+ })),
16
+ )
25
17
 
26
18
  fileStats.sort((a, b) => {
27
- // Sort so that files starting with 'assets/' come first
28
- if (a.file.startsWith('assets/') && !b.file.startsWith('assets/')) return -1
29
- if (!a.file.startsWith('assets/') && b.file.startsWith('assets/')) return 1
30
-
31
- // Within that, files ending with '.js' come first
32
- if (a.file.endsWith('.js') && !b.file.endsWith('.js')) return -1
33
- if (!a.file.endsWith('.js') && b.file.endsWith('.js')) return 1
34
-
35
- // Otherwise sort alphabetically
36
- return a.file.localeCompare(b.file)
19
+ const scoreA = (a.file.startsWith('assets/') ? 2 : 0) + (a.file.endsWith('.js') ? 1 : 0)
20
+ const scoreB = (b.file.startsWith('assets/') ? 2 : 0) + (b.file.endsWith('.js') ? 1 : 0)
21
+ return scoreB - scoreA || a.file.localeCompare(b.file)
37
22
  })
38
23
 
39
24
  return fileStats
40
25
  }
41
26
 
42
- async function generateFileStatsMarkdown(directory: string) {
43
- const fileStats = await getFileStats(directory)
44
- return [
45
- '| File | Size |',
46
- '| :--- | ---: |',
47
- ...fileStats.map(file => `| ${file.file} | ${filesize(file.size)} |`),
48
- ].join('\n')
49
- }
50
-
51
27
  async function main() {
52
28
  const { positionals: [directory] } = parseArgs({
53
29
  args: process.argv.slice(2),
@@ -61,7 +37,12 @@ async function main() {
61
37
 
62
38
  await x('bun', ['run', 'build'], { nodeOptions: { cwd: directory } })
63
39
 
64
- const markdownTable = await generateFileStatsMarkdown(path.join(directory, 'dist'))
40
+ const fileStats = await getFileStats(path.join(directory, 'dist'))
41
+ const markdownTable = [
42
+ '| File | Size |',
43
+ '| :--- | ---: |',
44
+ ...fileStats.map(file => `| ${file.file} | ${filesize(file.size)} |`),
45
+ ].join('\n')
65
46
  process.stdout.write(`${markdownTable}\n`)
66
47
  }
67
48
 
@@ -0,0 +1,330 @@
1
+ # Project Template
2
+
3
+ A full-stack application template with a Vue.js frontend, Bun-based API backend, PostgreSQL database, and Docker Compose setup for local development.
4
+
5
+ ## Overview
6
+
7
+ This template provides a modern full-stack application structure with:
8
+
9
+ - **Frontend (app)**: Vue 3 application with Vite, UnoCSS, TypeScript, and TanStack Query
10
+ - **Backend (api)**: Bun-based API server with oRPC, Better Auth, and Drizzle ORM
11
+ - **Database**: PostgreSQL with Drizzle migrations
12
+ - **Infrastructure**: Docker Compose with Traefik reverse proxy, PostgreSQL, and Metabase
13
+
14
+ ## Prerequisites
15
+
16
+ - [Bun](https://bun.sh/) (v1.3 or later)
17
+ - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
18
+ - [Node.js LTS](https://nodejs.org/en/download/) (for compatibility)
19
+ - [mkcert](https://github.com/FiloSottile/mkcert) (for local TLS certificates)
20
+
21
+ ## Project Structure
22
+
23
+ ```
24
+ project/
25
+ ├── api/ # Backend API server
26
+ │ ├── src/
27
+ │ │ ├── auth/ # Authentication setup (Better Auth)
28
+ │ │ ├── database/ # Database schema and migrations (Drizzle)
29
+ │ │ ├── orpc/ # Router definitions (oRPC)
30
+ │ │ ├── env.ts # Environment variable loading and validation (Valibot)
31
+ │ │ └── main.ts # API entry point
32
+ │ └── package.json
33
+ ├── app/ # Frontend Vue application
34
+ │ ├── src/
35
+ │ │ ├── components/ # Vue components
36
+ │ │ ├── composables/ # Vue composables (auth, etc.)
37
+ │ │ ├── lib/ # Library utilities (oRPC client)
38
+ │ │ └── main.ts # App entry point
39
+ │ └── package.json
40
+ ├── compose.yaml # Docker Compose configuration
41
+ └── package.json # Root workspace configuration
42
+ ```
43
+
44
+ ## Getting Started
45
+
46
+ ### 1. Install Dependencies
47
+
48
+ ```bash
49
+ bun install
50
+ ```
51
+
52
+ ### 2. Set Up Environment Variables
53
+
54
+ > **Note**: If `.env.development` (API) or `.env` (App) files already exist in the template, you may skip this step. Otherwise, create them as described below.
55
+
56
+ #### API Environment Variables
57
+
58
+ Create a `.env.development` file in the `api/` directory with the following variables:
59
+
60
+ ```bash
61
+ # api/.env.development
62
+ AUTH_SECRET=your-secret-key-here
63
+ DATABASE_URL=postgresql://user:password@postgres:5432/app
64
+ ALLOWED_ORIGINS=https://app.dev.localhost
65
+ LOG_LEVEL=info
66
+ HOST=0.0.0.0
67
+ PORT=4000
68
+ ```
69
+
70
+ **Required variables:**
71
+
72
+ - `AUTH_SECRET` - Secret key for authentication encryption
73
+ - `DATABASE_URL` - PostgreSQL connection string
74
+
75
+ **Optional variables:**
76
+
77
+ - `ALLOWED_ORIGINS` - Comma-separated list of allowed CORS origins (default: empty)
78
+ - `LOG_LEVEL` - Logging level: `fatal`, `error`, `warn`, `info`, `debug`, `trace`, or `silent` (default: `info`)
79
+ - `HOST` - Server host (default: `0.0.0.0`)
80
+ - `PORT` - Server port (default: `4000`)
81
+
82
+ #### App Environment Variables
83
+
84
+ Create a `.env` file in the `app/` directory with the following variables:
85
+
86
+ ```bash
87
+ # app/.env
88
+ VITE_API_URL=https://api.dev.localhost
89
+ ```
90
+
91
+ **Required variables:**
92
+
93
+ - `VITE_API_URL` - API base URL for the frontend
94
+
95
+ ### 3. Set Up Local TLS Certificates
96
+
97
+ To access the HTTPS URLs (`https://*.dev.localhost`), you need to set up trusted local TLS certificates using [mkcert](https://github.com/FiloSottile/mkcert).
98
+
99
+ Check their [installation guide](https://github.com/FiloSottile/mkcert#installation) for detailed instructions on installing mkcert.
100
+
101
+ Then, run the following command to generate the certificates:
102
+
103
+ ```bash
104
+ .docker/traefik/bin/install_cert
105
+ ```
106
+
107
+ This will generate the certificates in `.docker/traefik/certs/` and install the mkcert root CA into your system trust store.
108
+
109
+ > [!IMPORTANT]
110
+ > If your system is running on WSL and you are using Firefox or Zen Browser, **you must import the mkcert root CA into your browser trust store.**
111
+ >
112
+ > 1. Find the root CA file path:
113
+ > ```bash
114
+ > echo "wslpath -w $(mkcert -CAROOT)/rootCA.pem"
115
+ > # \\wsl.localhost\Ubuntu\home\user\.local\share\mkcert\rootCA.pem
116
+ > ```
117
+ > 2. Import the root CA file into your browser trust store:
118
+ > - **Settings** -> **Privacy & Security**.
119
+ > - **Certificates** -> **View Certificates...**
120
+ > - **Authorities** tab -> **Import...**
121
+ > - Select the rootCA.pem file (using the path found above)
122
+ > - Check **"Trust this CA to identify websites"**
123
+ > - Click **OK**
124
+
125
+ ### 4. Start Docker Services and Access the Application
126
+
127
+ Start all services (Traefik, PostgreSQL, Metabase, API, and App):
128
+
129
+ ```bash
130
+ docker compose up -d
131
+ ```
132
+
133
+ Once the services are running, you can access:
134
+
135
+ - Frontend: https://app.dev.localhost
136
+ - API: https://api.dev.localhost
137
+ - Traefik Dashboard: https://traefik.dev.localhost
138
+ - Metabase: https://metabase.dev.localhost
139
+
140
+ ### 5. Stopping Services
141
+
142
+ To stop all Docker services:
143
+
144
+ ```bash
145
+ docker compose down
146
+ ```
147
+
148
+ To stop and remove volumes (⚠️ this will delete database data):
149
+
150
+ ```bash
151
+ docker compose down -v
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ### Running Services Locally (without Docker)
157
+
158
+ You can also run the services locally without Docker. Note that you'll still need PostgreSQL running (either locally or via Docker).
159
+
160
+ #### API Server
161
+
162
+ ```bash
163
+ cd api
164
+ bun run dev
165
+ ```
166
+
167
+ The API will run on `http://localhost:4000` (or the port specified in `PORT`).
168
+
169
+ #### Frontend App
170
+
171
+ ```bash
172
+ cd app
173
+ bun run dev
174
+ ```
175
+
176
+ The app will run on `http://localhost:5173` (Vite default port).
177
+
178
+ ### Database Management
179
+
180
+ #### Generate Migrations
181
+
182
+ After modifying the database schema in `api/src/database/schema/`, generate migrations:
183
+
184
+ ```bash
185
+ cd api
186
+ bun run db:generate
187
+ ```
188
+
189
+ #### Run Migrations
190
+
191
+ Migrations run automatically when starting the API in dev mode. To run manually:
192
+
193
+ ```bash
194
+ cd api
195
+ bun run db:migrate
196
+ ```
197
+
198
+ ## Available Scripts
199
+
200
+ ### Root Level Scripts
201
+
202
+ - `bun run check` - Run all checks (ESLint, Stylelint, unused dependencies, TypeScript)
203
+ - `bun run lint` - Run linting (ESLint and Stylelint)
204
+ - `bun run lint:inspect` - Open ESLint config inspector
205
+ - `bun run update` - Update dependencies
206
+
207
+ ### API Scripts (`cd api`)
208
+
209
+ - `bun run dev` - Start development server with hot reload
210
+ - `bun run build` - Build production binary
211
+ - `bun run db:generate` - Generate database migrations
212
+ - `bun run db:migrate` - Run database migrations
213
+
214
+ ### App Scripts (`cd app`)
215
+
216
+ - `bun run dev` - Start development server with HMR (Hot Module Replacement)
217
+ - `bun run build` - Build for production (static site)
218
+ - `bun run build:analyze` - Build with bundle analyzer
219
+ - `bun run preview` - Preview production build
220
+
221
+ ## Docker Compose Services
222
+
223
+ ### Traefik
224
+
225
+ Reverse proxy and load balancer that handles:
226
+
227
+ - HTTPS termination
228
+ - SSL certificate management (requires certificates in `.docker/traefik/certs/`)
229
+ - Routing to services based on hostnames
230
+ - Dashboard accessible at https://traefik.dev.localhost
231
+
232
+ ### PostgreSQL
233
+
234
+ Database service with:
235
+
236
+ - Default database: `app`
237
+ - Default user: `user`
238
+ - Default password: `password`
239
+ - Port: `5432`
240
+ - Persistent volume: `postgres_data`
241
+
242
+ ### Metabase
243
+
244
+ Business intelligence tool for data visualization and analytics with:
245
+
246
+ - Persistent volume: `metabase_data`
247
+ - Accessible via Traefik at https://metabase.dev.localhost (runs on port `3000` internally)
248
+
249
+ ### API
250
+
251
+ Backend API service running Bun with:
252
+
253
+ - Hot reload enabled
254
+ - Automatic database migrations on startup
255
+ - Health check endpoint: `/health`
256
+ - Auth endpoints: `/auth/*`
257
+ - RPC endpoints: `/rpc/*`
258
+ - Accessible via Traefik at https://api.dev.localhost (runs on port `4000` internally)
259
+
260
+ ### App
261
+
262
+ Frontend Vue application with:
263
+
264
+ - Vite dev server
265
+ - Hot module replacement
266
+ - Accessible via Traefik at https://app.dev.localhost (runs on port `5173` internally)
267
+
268
+ ## Environment Variables
269
+
270
+ For detailed environment variable setup, see [Set Up Environment Variables](#2-set-up-environment-variables) in the Getting Started section.
271
+
272
+ **Quick reference:**
273
+
274
+ - **API** (`.env.development`): `AUTH_SECRET` (required), `DATABASE_URL` (required), `ALLOWED_ORIGINS`, `LOG_LEVEL`, `HOST`, `PORT`
275
+ - **App** (`.env`): `VITE_API_URL` (required)
276
+
277
+ ## Building for Production
278
+
279
+ ### Build API
280
+
281
+ ```bash
282
+ cd api
283
+ bun run build
284
+ ```
285
+
286
+ This creates a compiled binary at `api/dist/api`.
287
+
288
+ ### Build App
289
+
290
+ ```bash
291
+ cd app
292
+ bun run build
293
+ ```
294
+
295
+ This creates a static site in `app/dist/`.
296
+
297
+ ## Troubleshooting
298
+
299
+ ### Port Already in Use
300
+
301
+ If ports 80, 443, or 5432 are already in use, modify the port mappings in `compose.yaml`.
302
+
303
+ ### SSL Certificate Warnings
304
+
305
+ If you see SSL certificate warnings when accessing `https://*.dev.localhost` URLs, ensure you have completed the [Local TLS Setup](#3-set-up-local-tls-certificates) to generate trusted certificates using mkcert.
306
+
307
+ **Note**: Without proper certificates, Traefik may fail to start or services may not be accessible via HTTPS.
308
+
309
+ ### Service Logs
310
+
311
+ To view logs for a specific service:
312
+
313
+ ```bash
314
+ docker compose logs -f <service-name>
315
+ ```
316
+
317
+ For example:
318
+
319
+ - `docker compose logs -f api` - View API logs
320
+ - `docker compose logs -f app` - View app logs
321
+ - `docker compose logs -f traefik` - View Traefik logs
322
+
323
+ ## Tech Stack
324
+
325
+ - **Runtime**: Bun
326
+ - **Language**: TypeScript
327
+ - **Frontend**: Vue 3, Vite, UnoCSS, TanStack Query
328
+ - **Backend**: Bun, oRPC, Better Auth, Drizzle ORM
329
+ - **Database**: PostgreSQL
330
+ - **Infrastructure**: Docker Compose, Traefik
@@ -1,4 +1,4 @@
1
- ALLOWED_ORIGINS=https://app.dev.localhost
2
- AUTH_SECRET=foo
3
- DATABASE_URL=.db
4
- NODE_ENV=development
1
+ ALLOWED_ORIGINS="https://app.dev.localhost"
2
+ AUTH_SECRET="D0QykrmzDgVrP4GCKkFPT1CB1UplFk4bhIJk92SUtSQ="
3
+ DATABASE_URL="postgresql://user:password@postgres:5432/app"
4
+ NODE_ENV="development"
@@ -3,12 +3,13 @@
3
3
  "type": "module",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "bun --watch --no-clear-screen src/main.ts | pino-pretty",
6
+ "dev": "bun run db:migrate 1> /dev/null && bun --watch --no-clear-screen src/main.ts | pino-pretty",
7
7
  "build": "bun build src/main.ts --compile --minify --sourcemap --outfile dist/api",
8
8
  "db:generate": "bun --bun run drizzle-kit generate --config src/database/drizzle/config.ts",
9
9
  "db:migrate": "bun --bun run drizzle-kit migrate --config src/database/drizzle/config.ts"
10
10
  },
11
11
  "dependencies": {
12
+ "@libsql/client": "^0.15.15",
12
13
  "@orpc/server": "^1.11.2",
13
14
  "better-auth": "^1.3.34",
14
15
  "drizzle-orm": "^0.44.7",
@@ -16,9 +17,9 @@
16
17
  "valibot": "^1.1.0"
17
18
  },
18
19
  "devDependencies": {
19
- "@electric-sql/pglite": "^0.3.14",
20
20
  "@types/bun": "^1.3.2",
21
21
  "drizzle-kit": "^0.31.7",
22
+ "pg": "^8.16.3",
22
23
  "pino-pretty": "^13.1.2"
23
24
  }
24
25
  }
@@ -2,10 +2,13 @@ import { betterAuth } from 'better-auth'
2
2
  import { type DB, drizzleAdapter } from 'better-auth/adapters/drizzle'
3
3
  import type { BaseLogger } from 'pino'
4
4
 
5
+ import { env } from '../env'
6
+
5
7
  export function createBetterAuth({ db, logger }: { db: DB, logger: BaseLogger }) {
6
8
  return betterAuth({
7
9
  basePath: '/auth',
8
- trustedOrigins: import.meta.env.ALLOWED_ORIGINS.split(','),
10
+ secret: env.auth.secret,
11
+ trustedOrigins: env.cors.allowedOrigins,
9
12
  database: drizzleAdapter(db, {
10
13
  provider: 'pg',
11
14
  usePlural: true,
@@ -1,10 +1,11 @@
1
1
  import { defineConfig } from 'drizzle-kit'
2
2
 
3
+ import { env } from '../../env'
4
+
3
5
  export default defineConfig({
4
6
  casing: 'snake_case',
5
7
  dialect: 'postgresql',
6
- driver: 'pglite',
7
- dbCredentials: { url: import.meta.env.DATABASE_URL },
8
+ dbCredentials: { url: env.database.url },
8
9
  schema: 'src/database/schema',
9
10
  out: 'src/database/migrations',
10
11
  })
@@ -1,11 +1,13 @@
1
- import { PGlite } from '@electric-sql/pglite'
2
- import { drizzle } from 'drizzle-orm/pglite'
1
+ import { drizzle } from 'drizzle-orm/bun-sql'
3
2
 
3
+ import { env } from '../env'
4
4
  import { logger } from '../utils/logger'
5
5
  import * as schema from './schema'
6
6
 
7
7
  export const db = drizzle({
8
- client: new PGlite(import.meta.env.DATABASE_URL),
8
+ connection: {
9
+ url: env.database.url,
10
+ },
9
11
  casing: 'snake_case',
10
12
  schema,
11
13
  logger: {
@@ -0,0 +1,67 @@
1
+ import * as v from 'valibot'
2
+
3
+ const schema = v.object({
4
+ auth: v.object({
5
+ secret: v.string(),
6
+ }),
7
+ cors: v.object({
8
+ allowedOrigins: v.pipe(
9
+ v.optional(v.string(), ''),
10
+ v.transform(input => input.split(',').filter(Boolean)),
11
+ ),
12
+ }),
13
+ database: v.object({
14
+ url: v.string(),
15
+ }),
16
+ log: v.object({
17
+ level: v.optional(v.union([
18
+ v.literal('fatal'),
19
+ v.literal('error'),
20
+ v.literal('warn'),
21
+ v.literal('info'),
22
+ v.literal('debug'),
23
+ v.literal('trace'),
24
+ v.literal('silent'),
25
+ ]), 'info'),
26
+ }),
27
+ server: v.object({
28
+ host: v.optional(v.string(), '0.0.0.0'),
29
+ port: v.pipe(
30
+ v.optional(v.string(), '4000'),
31
+ v.decimal(),
32
+ v.transform(input => Number(input)),
33
+ ),
34
+ }),
35
+ })
36
+
37
+ const parsed = v.safeParse(schema, {
38
+ auth: {
39
+ secret: import.meta.env.AUTH_SECRET,
40
+ },
41
+ cors: {
42
+ allowedOrigins: import.meta.env.ALLOWED_ORIGINS,
43
+ },
44
+ database: {
45
+ url: import.meta.env.DATABASE_URL,
46
+ },
47
+ log: {
48
+ level: import.meta.env.LOG_LEVEL,
49
+ },
50
+ server: {
51
+ host: import.meta.env.HOST,
52
+ port: import.meta.env.PORT,
53
+ },
54
+ })
55
+
56
+ if (!parsed.success) {
57
+ throw new Error([
58
+ '🚨 Environment Validation Error!',
59
+ ...parsed.issues.map(issue =>
60
+ issue.path
61
+ ? `[${issue.path.map(p => p.key).join('.')}] ${issue.message}`
62
+ : issue.message,
63
+ ),
64
+ ].join('\n'))
65
+ }
66
+
67
+ export const env = parsed.output
@@ -2,6 +2,7 @@ import process from 'node:process'
2
2
 
3
3
  import { createBetterAuth } from './auth'
4
4
  import { db } from './database'
5
+ import { env } from './env'
5
6
  import { createRpc } from './orpc'
6
7
  import { router } from './orpc/router'
7
8
  import { cors } from './utils/cors'
@@ -11,9 +12,10 @@ const auth = createBetterAuth({ db, logger })
11
12
  const rpc = createRpc({ auth, db, logger }, router)
12
13
 
13
14
  const server = Bun.serve({
14
- hostname: import.meta.env.HOST ?? '0.0.0.0',
15
- port: import.meta.env.PORT ?? 4000,
15
+ hostname: env.server.host,
16
+ port: env.server.port,
16
17
  routes: {
18
+ '/health': () => new Response('OK', { status: 200 }),
17
19
  '/auth/*': cors(auth.handler),
18
20
  '/rpc/*': cors(rpc.handler),
19
21
  },
@@ -1,10 +1,10 @@
1
- export function cors(handler: (req: Request) => Promise<Response>) {
2
- const allowedOrigins = import.meta.env.ALLOWED_ORIGINS.split(',')
1
+ import { env } from '../env'
3
2
 
3
+ export function cors(handler: (req: Request) => Promise<Response>) {
4
4
  return async (req: Request) => {
5
- const origin = req.headers.get('origin') ?? ''
5
+ const origin = req.headers.get('origin')
6
6
 
7
- if (!origin || !allowedOrigins.includes(origin)) {
7
+ if (origin && !env.cors.allowedOrigins.includes(origin)) {
8
8
  return new Response('Origin not allowed', { status: 403 })
9
9
  }
10
10
 
@@ -19,7 +19,10 @@ export function cors(handler: (req: Request) => Promise<Response>) {
19
19
  }
20
20
 
21
21
  response.headers.append('Access-Control-Allow-Credentials', 'true')
22
- response.headers.append('Access-Control-Allow-Origin', origin)
22
+
23
+ if (origin) {
24
+ response.headers.append('Access-Control-Allow-Origin', origin)
25
+ }
23
26
 
24
27
  return response
25
28
  }
@@ -1,7 +1,9 @@
1
1
  import pino from 'pino'
2
2
 
3
+ import { env } from '../env'
4
+
3
5
  export const logger = pino({
4
- level: import.meta.env.LOG_LEVEL ?? 'info',
6
+ level: env.log.level,
5
7
  base: {},
6
8
  })
7
9
 
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="description" content="Description" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="color-scheme" content="dark light" />
7
8
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
9
  <title>Title</title>
9
10
  </head>
@@ -12,8 +12,8 @@
12
12
  "@kevinmarrec/vue-i18n": "^1.1.3",
13
13
  "@orpc/client": "^1.11.2",
14
14
  "@orpc/tanstack-query": "1.11.2",
15
- "@tanstack/query-core": "^5.90.9",
16
- "@tanstack/vue-query": "^5.91.1",
15
+ "@tanstack/query-core": "^5.90.10",
16
+ "@tanstack/vue-query": "^5.91.2",
17
17
  "@unhead/vue": "^2.0.19",
18
18
  "@vueuse/core": "^14.0.0",
19
19
  "better-auth": "^1.3.34",
@@ -10,6 +10,7 @@ const link = new RPCLink({
10
10
  globalThis.fetch(request, {
11
11
  ...init,
12
12
  credentials: 'include',
13
+ signal: AbortSignal.timeout(30_000),
13
14
  }),
14
15
  })
15
16
 
@@ -13,8 +13,9 @@ services:
13
13
  - no-new-privileges:true
14
14
  command:
15
15
  # General configuration
16
- - --log.level=INFO
17
16
  - --api.dashboard=true
17
+ - --ping=true
18
+ - --log.level=INFO
18
19
  # Entrypoints for HTTP and HTTPS
19
20
  - --entrypoints.websecure.address=:443
20
21
  - --entrypoints.web.address=:80
@@ -26,24 +27,70 @@ services:
26
27
  - --providers.docker.exposedbydefault=false
27
28
  - --providers.file.directory=/etc/traefik/dynamic
28
29
  - --providers.file.watch=true
30
+ ports:
31
+ - 80:80
32
+ - 443:443
33
+ volumes:
34
+ - /var/run/docker.sock:/var/run/docker.sock:ro
35
+ - ./.docker/traefik/dynamic:/etc/traefik/dynamic:ro # Mount the dynamic config directory
36
+ - ./.docker/traefik/certs:/certs:ro # Mount the certs directory
37
+ healthcheck:
38
+ test: [CMD-SHELL, traefik healthcheck --ping]
39
+ interval: 1s
40
+ timeout: 1s
41
+ retries: 10
29
42
  labels:
30
43
  traefik.enable: 'true'
31
44
  traefik.http.routers.traefik.rule: Host(`traefik.dev.localhost`)
32
45
  traefik.http.routers.traefik.entrypoints: websecure
33
46
  traefik.http.routers.traefik.tls: 'true'
34
47
  traefik.http.routers.traefik.service: api@internal
48
+
49
+ postgres:
50
+ image: postgres:18-alpine
51
+ container_name: postgres
52
+ environment:
53
+ - POSTGRES_USER=user
54
+ - POSTGRES_PASSWORD=password
55
+ - POSTGRES_DB=app
35
56
  ports:
36
- - 80:80
37
- - 443:443
57
+ - 5432:5432
38
58
  volumes:
39
- - /var/run/docker.sock:/var/run/docker.sock:ro
40
- - ./.docker/traefik/dynamic:/etc/traefik/dynamic:ro # Mount the dynamic config directory
41
- - ./.docker/traefik/certs:/certs:ro # Mount the certs directory
59
+ - postgres_data:/var/lib/postgresql/data
60
+ healthcheck:
61
+ test: [CMD-SHELL, "psql postgresql://user:password@postgres:5432/app -c 'SELECT 1' 2> /dev/null"]
62
+ interval: 1s
63
+ timeout: 1s
64
+ retries: 10
65
+
66
+ metabase:
67
+ image: metabase/metabase:v0.57.x
68
+ container_name: metabase
69
+ environment:
70
+ - MB_DB_TYPE=h2
71
+ - MB_DB_FILE=/var/lib/metabase/metabase.db
72
+ - MB_SITE_URL=https://metabase.dev.localhost
73
+ volumes:
74
+ - metabase_data:/var/lib/metabase
75
+ healthcheck:
76
+ test: [CMD-SHELL, curl -f http://localhost:3000/api/health]
77
+ interval: 1s
78
+ timeout: 1s
79
+ retries: 20
80
+ labels:
81
+ traefik.enable: 'true'
82
+ traefik.http.routers.metabase.rule: Host(`metabase.dev.localhost`)
83
+ traefik.http.routers.metabase.entrypoints: websecure
84
+ traefik.http.routers.metabase.tls: 'true'
85
+ traefik.http.services.metabase.loadbalancer.server.port: '3000'
42
86
 
43
87
  api:
44
88
  <<: *common
45
89
  depends_on:
46
- - traefik
90
+ traefik:
91
+ condition: service_healthy
92
+ postgres:
93
+ condition: service_healthy
47
94
  container_name: api
48
95
  image: oven/bun:1-alpine
49
96
  init: true
@@ -73,3 +120,11 @@ services:
73
120
  traefik.http.routers.app.entrypoints: websecure
74
121
  traefik.http.routers.app.tls: 'true'
75
122
  traefik.http.services.app.loadbalancer.server.port: '5173'
123
+
124
+ networks:
125
+ default:
126
+ name: dev
127
+
128
+ volumes:
129
+ postgres_data:
130
+ metabase_data:
@@ -1,7 +1,9 @@
1
1
  import type { KnipConfig } from 'knip'
2
2
 
3
+ // Required for Knip to pass
3
4
  Object.assign(import.meta.env, {
4
- DATABASE_URL: 'foo.db',
5
+ AUTH_SECRET: '',
6
+ DATABASE_URL: '',
5
7
  })
6
8
 
7
9
  export default {
@@ -31,6 +31,6 @@
31
31
  "tinyexec": "^1.0.2",
32
32
  "tinyglobby": "^0.2.15",
33
33
  "typescript": "~5.9.3",
34
- "vue-tsc": "^3.1.3"
34
+ "vue-tsc": "^3.1.4"
35
35
  }
36
36
  }
@@ -1,11 +0,0 @@
1
- interface ImportMetaEnv {
2
- readonly ALLOWED_ORIGINS: string
3
- readonly AUTH_SECRET: string
4
- readonly DATABASE_URL: string
5
- readonly LOG_LEVEL: string
6
- readonly NODE_ENV: string
7
- }
8
-
9
- interface ImportMeta {
10
- readonly env: ImportMetaEnv
11
- }
@@ -1,152 +0,0 @@
1
- # **Local TLS Setup Guide**
2
-
3
- This guide provides the essential steps to install mkcert and generate trusted TLS certificates for your Traefik-secured local development environment.
4
-
5
- Certificates are expected in `.docker/traefik/certs/`.
6
-
7
- ## **Prerequisites**
8
-
9
- - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed
10
- - Ports 80 and 443 available on your system
11
- - Administrator/sudo access for installing mkcert
12
-
13
- ## **1. Install mkcert (Ubuntu/WSL)**
14
-
15
- Download, install, and clean up the executable:
16
-
17
- ```sh
18
- curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
19
- chmod +x mkcert-v*-linux-amd64
20
- sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
21
- rm mkcert-v*-linux-amd64
22
- ```
23
-
24
- Verify the installation:
25
-
26
- ```sh
27
- mkcert -version
28
- ```
29
-
30
- ## **2. Install the mkcert Root CA**
31
-
32
- Install the local root CA to trust generated certificates:
33
-
34
- ```sh
35
- mkcert -install
36
- ```
37
-
38
- ### **Firefox and Zen Browser (Manual Import)**
39
-
40
- Browsers like Firefox and Firefox-based Zen Browser require manual import into their internal trust stores.
41
-
42
- 1. **Find Root CA File Path:**
43
- Run the appropriate command below to get the full path to the rootCA.pem file:
44
- - **Linux Native (or WSL for Linux Apps):**
45
-
46
- ```sh
47
- echo "$(mkcert -CAROOT)/rootCA.pem"
48
- # /home/user/.local/share/mkcert/rootCA.pem
49
- ```
50
-
51
- - **WSL for Windows Host Applications:** Use wslpath to get the Windows-formatted path.
52
- ```sh
53
- wslpath -w "$(mkcert -CAROOT)/rootCA.pem"
54
- # \\wsl.localhost\Ubuntu\home\user\.local\share\mkcert\rootCA.pem
55
- ```
56
-
57
- 2. **Import:** In the browser's settings:
58
- - **Settings** -> **Privacy & Security**.
59
- - **Certificates** -> **View Certificates...**
60
- - **Authorities** tab -> **Import...**
61
- - Select the rootCA.pem file (using the path found above)
62
- - Check **"Trust this CA to identify websites"**
63
- - Click **OK**
64
-
65
- ## **3. Generate the `dev.localhost` Certificate**
66
-
67
- Generate the wildcard certificate for `*.dev.localhost` and place the files where Traefik is configured to look (See [`tls.yml`](../.docker/traefik/dynamic/tls.yml)).
68
-
69
- 1. **Create Directory:**
70
-
71
- ```sh
72
- mkdir -p .docker/traefik/certs
73
- ```
74
-
75
- 2. **Generate Certificate:**
76
-
77
- ```sh
78
- mkcert \
79
- -cert-file .docker/traefik/certs/dev.localhost.crt \
80
- -key-file .docker/traefik/certs/dev.localhost.key \
81
- "dev.localhost" "*.dev.localhost"
82
- ```
83
-
84
- 3. **Verify Certificate:**
85
-
86
- ```sh
87
- openssl x509 -in .docker/traefik/certs/dev.localhost.crt -text -noout | grep -A 2 "Subject Alternative Name"
88
- ```
89
-
90
- ## **4. Validate Setup**
91
-
92
- 1. **Start Services:**
93
-
94
- ```sh
95
- docker compose up -d
96
- ```
97
-
98
- 2. **Test in Browser:**
99
-
100
- - Visit **https://traefik.dev.localhost** — you should see the Traefik dashboard
101
- - Visit **https://app.dev.localhost** — you should see your app
102
- - Both should load without SSL warnings
103
-
104
- ## **5. Troubleshooting**
105
-
106
- ### **Certificate Files Missing**
107
-
108
- If Traefik fails to start or shows TLS errors, verify the certificate files exist:
109
-
110
- ```sh
111
- ls -la .docker/traefik/certs/
112
- ```
113
-
114
- ### **Browser Shows SSL Warning**
115
-
116
- - Ensure mkcert root CA is installed (`mkcert -install`)
117
- - For Firefox/Zen Browser, manually import the root CA (see section 2)
118
- - Clear browser cache and restart the browser
119
-
120
- ### **Certificate Expiration**
121
-
122
- mkcert certificates are valid for a long time, but if you need to regenerate:
123
-
124
- 1. **Remove old certificates:**
125
-
126
- ```sh
127
- rm .docker/traefik/certs/dev.localhost.*
128
- ```
129
-
130
- 2. **Regenerate (see section 3)**
131
-
132
- ## **6. Clean Up**
133
-
134
- 1. **Stop Services:**
135
-
136
- ```sh
137
- docker compose down
138
- ```
139
-
140
- 2. **Remove Certificates (Optional):**
141
-
142
- ```sh
143
- rm -rf .docker/traefik/certs
144
- ```
145
-
146
- 3. **Uninstall mkcert Root CA (Optional):**
147
-
148
- ```sh
149
- mkcert -uninstall
150
- ```
151
-
152
- > **Note:** Removing the root CA will cause SSL warnings in browsers until you reinstall it or regenerate certificates.