@kevinmarrec/create-app 0.9.0 → 0.10.0
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 +2 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/template/.docker/traefik/bin/install_cert +18 -0
- package/template/.docker/traefik/bin/uninstall_cert +9 -0
- package/template/README.md +330 -0
- package/template/api/.env.development +4 -4
- package/template/api/package.json +3 -2
- package/template/api/src/auth/index.ts +4 -1
- package/template/api/src/database/drizzle/config.ts +3 -2
- package/template/api/src/database/index.ts +5 -3
- package/template/api/src/env.ts +67 -0
- package/template/api/src/main.ts +4 -2
- package/template/api/src/utils/cors.ts +8 -5
- package/template/api/src/utils/logger.ts +3 -1
- package/template/app/index.html +1 -0
- package/template/app/package.json +2 -2
- package/template/app/src/lib/orpc.ts +1 -0
- package/template/compose.yaml +62 -7
- package/template/knip.config.ts +3 -1
- package/template/package.json +1 -1
- package/template/api/src/env.d.ts +0 -11
- package/template/docs/local-tls.md +0 -152
package/README.md
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevinmarrec/create-app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
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.
|
|
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,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=
|
|
3
|
-
DATABASE_URL
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
package/template/api/src/main.ts
CHANGED
|
@@ -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:
|
|
15
|
-
port:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
22
|
+
|
|
23
|
+
if (origin) {
|
|
24
|
+
response.headers.append('Access-Control-Allow-Origin', origin)
|
|
25
|
+
}
|
|
23
26
|
|
|
24
27
|
return response
|
|
25
28
|
}
|
package/template/app/index.html
CHANGED
|
@@ -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.
|
|
16
|
-
"@tanstack/vue-query": "^5.91.
|
|
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",
|
package/template/compose.yaml
CHANGED
|
@@ -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
|
-
-
|
|
37
|
-
- 443:443
|
|
57
|
+
- 5432:5432
|
|
38
58
|
volumes:
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
-
|
|
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
|
-
|
|
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:
|
package/template/knip.config.ts
CHANGED
package/template/package.json
CHANGED
|
@@ -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.
|