@fluid-app/fluid-cli-portal 0.1.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.
Files changed (45) hide show
  1. package/dist/index.d.mts +680 -0
  2. package/dist/index.d.mts.map +1 -0
  3. package/dist/index.mjs +2230 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/package.json +54 -0
  6. package/templates/base/index.html +14 -0
  7. package/templates/base/src/App.tsx +18 -0
  8. package/templates/base/src/index.css +80 -0
  9. package/templates/base/src/main.tsx +42 -0
  10. package/templates/base/src/navigation.config.ts +21 -0
  11. package/templates/base/src/portal.config.ts +32 -0
  12. package/templates/base/src/screens/Dashboard.tsx +133 -0
  13. package/templates/base/src/screens/ExampleForm.tsx +187 -0
  14. package/templates/base/src/vite-env.d.ts +1 -0
  15. package/templates/base/tsconfig.json +26 -0
  16. package/templates/fullstack/.dockerignore +9 -0
  17. package/templates/fullstack/.env.example +15 -0
  18. package/templates/fullstack/.github/workflows/ci.yml +47 -0
  19. package/templates/fullstack/.github/workflows/deploy.yml +54 -0
  20. package/templates/fullstack/Dockerfile +44 -0
  21. package/templates/fullstack/README.md.template +176 -0
  22. package/templates/fullstack/drizzle/0000_initial.sql +7 -0
  23. package/templates/fullstack/drizzle/meta/0000_snapshot.json +63 -0
  24. package/templates/fullstack/drizzle/meta/_journal.json +13 -0
  25. package/templates/fullstack/drizzle.config.ts +13 -0
  26. package/templates/fullstack/esbuild.config.js +14 -0
  27. package/templates/fullstack/eslint.config.js +13 -0
  28. package/templates/fullstack/package.json.template +63 -0
  29. package/templates/fullstack/src/fluid.config.ts.template +69 -0
  30. package/templates/fullstack/src/server/db/index.ts +10 -0
  31. package/templates/fullstack/src/server/db/migrate.ts +12 -0
  32. package/templates/fullstack/src/server/db/schema.ts +14 -0
  33. package/templates/fullstack/src/server/entry.ts +59 -0
  34. package/templates/fullstack/src/server/index.ts +33 -0
  35. package/templates/fullstack/src/server/routes/index.test.ts +123 -0
  36. package/templates/fullstack/src/server/routes/index.ts +109 -0
  37. package/templates/fullstack/src/server/routes/schemas.ts +7 -0
  38. package/templates/fullstack/src/test/setup.ts +9 -0
  39. package/templates/fullstack/vite.config.ts +39 -0
  40. package/templates/fullstack/vitest.config.ts +9 -0
  41. package/templates/starter/.env.example +1 -0
  42. package/templates/starter/README.md.template +218 -0
  43. package/templates/starter/package.json.template +40 -0
  44. package/templates/starter/src/fluid.config.ts.template +69 -0
  45. package/templates/starter/vite.config.ts +12 -0
@@ -0,0 +1,44 @@
1
+ FROM node:20-slim AS builder
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package.json pnpm-lock.yaml ./
7
+
8
+ # Install dependencies
9
+ RUN corepack enable && pnpm install --frozen-lockfile
10
+
11
+ # Copy source code
12
+ COPY . .
13
+
14
+ # Build frontend (Vite) and server (esbuild)
15
+ RUN pnpm run build
16
+
17
+ # Collect production files into a staging directory for clean cross-stage copy
18
+ RUN mkdir /staging && cp package.json pnpm-lock.yaml /staging/
19
+
20
+ # --- Production stage ---
21
+ FROM node:20-slim AS runner
22
+
23
+ WORKDIR /app
24
+
25
+ ENV NODE_ENV=production
26
+ ENV PORT=8080
27
+
28
+ # Copy build outputs and migration files
29
+ COPY --from=builder /app/dist ./dist
30
+ COPY --from=builder /app/drizzle ./drizzle
31
+
32
+ # Copy package + lock files for production install
33
+ COPY --from=builder /staging ./
34
+
35
+ # Install production dependencies only (@libsql/client needs node_modules)
36
+ RUN corepack enable && pnpm install --prod --frozen-lockfile
37
+
38
+ RUN addgroup --system --gid 1001 appgroup && \
39
+ adduser --system --uid 1001 --ingroup appgroup appuser
40
+ USER appuser
41
+
42
+ EXPOSE 8080
43
+
44
+ CMD ["node", "dist/server/entry.js"]
@@ -0,0 +1,176 @@
1
+ # {{projectName}}
2
+
3
+ A Fluid Commerce Portal built with the **Fullstack** template — includes [Hono](https://hono.dev/) API server, [Drizzle ORM](https://orm.drizzle.team/), and SQLite for a complete frontend + backend setup.
4
+
5
+ ## What's Included
6
+
7
+ ### Frontend
8
+
9
+ - [React 19](https://react.dev/) with [TypeScript](https://www.typescriptlang.org/)
10
+ - [Vite](https://vite.dev/) for fast development and optimized builds
11
+ - [Tailwind CSS 4](https://tailwindcss.com/) for utility-first styling
12
+ - [TanStack Query](https://tanstack.com/query) for data fetching and caching
13
+ - [Fluid Portal SDK](https://github.com/fluidcommerce) for Fluid Commerce integration
14
+ - Pre-configured navigation, screen routing, and dashboard widgets
15
+
16
+ ### Backend
17
+
18
+ - [Hono](https://hono.dev/) — lightweight, fast API server integrated with Vite dev server
19
+ - [Drizzle ORM](https://orm.drizzle.team/) — type-safe SQL with zero overhead
20
+ - [libSQL](https://github.com/tursodatabase/libsql-client-ts) — SQLite-compatible database, works locally and with [Turso](https://turso.tech/)
21
+ - Example CRUD routes (`/api/todos`) to get started quickly
22
+
23
+ ## Getting Started
24
+
25
+ ```bash
26
+ # Install dependencies
27
+ pnpm install
28
+
29
+ # Push the database schema (creates the SQLite database)
30
+ pnpm db:push
31
+
32
+ # Start the development server
33
+ pnpm dev
34
+ ```
35
+
36
+ Open [http://localhost:5173](http://localhost:5173) to view your portal. _(Port may differ if 5173 is in use — check the dev server output.)_
37
+
38
+ ### Other Commands
39
+
40
+ ```bash
41
+ pnpm build # Build for production (frontend + server)
42
+ pnpm start # Start the production server
43
+ pnpm preview # Preview production build via Vite
44
+ pnpm typecheck # Run TypeScript type checking
45
+ pnpm lint # Run ESLint
46
+ ```
47
+
48
+ ## Database Setup
49
+
50
+ This template uses SQLite with Drizzle ORM. The database is stored as a local file (`local.db`).
51
+
52
+ ```bash
53
+ # Create or update the database schema from your Drizzle schema definitions
54
+ pnpm db:push
55
+
56
+ # Open Drizzle Studio to browse and edit your data
57
+ pnpm db:studio
58
+
59
+ # Generate SQL migration files (for production workflows)
60
+ pnpm db:generate
61
+
62
+ # Run pending migrations
63
+ pnpm db:migrate
64
+ ```
65
+
66
+ Edit the schema in `src/server/db/schema.ts`, then run `pnpm db:push` to apply changes during development.
67
+
68
+ ## Project Structure
69
+
70
+ ```
71
+ src/
72
+ ├── main.tsx # App entry with FluidProvider setup
73
+ ├── App.tsx # Main app with screen routing
74
+ ├── index.css # Global styles (Tailwind CSS)
75
+ ├── fluid.config.ts # Fluid SDK configuration
76
+ ├── portal.config.ts # Navigation and screen registration
77
+ ├── navigation/
78
+ │ └── index.tsx # Sidebar navigation component
79
+ ├── screens/
80
+ │ └── Dashboard.tsx # Dashboard with widgets
81
+ └── server/
82
+ ├── index.ts # Hono app (mounts routes, exported for dev + prod)
83
+ ├── entry.ts # Production server entry (Node.js + static serving)
84
+ ├── routes/
85
+ │ └── index.ts # Example CRUD routes (/api/todos)
86
+ └── db/
87
+ ├── index.ts # Database connection
88
+ ├── migrate.ts # Auto-migration on server start
89
+ └── schema.ts # Drizzle schema definitions
90
+ ```
91
+
92
+ ## Adding API Routes
93
+
94
+ Create a new route file in `src/server/routes/` and mount it in `src/server/index.ts`:
95
+
96
+ ```typescript
97
+ // src/server/routes/products.ts
98
+ import { Hono } from "hono";
99
+
100
+ export const productRoutes = new Hono();
101
+
102
+ productRoutes.get("/", async (c) => {
103
+ // your logic here
104
+ return c.json({ products: [] });
105
+ });
106
+ ```
107
+
108
+ ```typescript
109
+ // src/server/index.ts
110
+ import { productRoutes } from "./routes/products";
111
+
112
+ app.route("/api/products", productRoutes);
113
+ ```
114
+
115
+ ## Using Turso (Cloud Database)
116
+
117
+ This template uses `@libsql/client`, which works with both local SQLite files and [Turso](https://turso.tech/) cloud databases. To use Turso:
118
+
119
+ 1. Create a Turso database: `turso db create my-app`
120
+ 2. Get the connection URL: `turso db show my-app --url`
121
+ 3. Create an auth token: `turso db tokens create my-app`
122
+ 4. Set `DATABASE_URL` and `DATABASE_AUTH_TOKEN` in your `.env` file
123
+
124
+ ## Docker
125
+
126
+ Build and run with Docker:
127
+
128
+ ```bash
129
+ docker build -t {{projectName}} .
130
+ docker run -p 8080:8080 -e DATABASE_URL=<your-turso-url> -e DATABASE_AUTH_TOKEN=<your-token> {{projectName}}
131
+ ```
132
+
133
+ Database migrations run automatically when the server starts, so the database schema is always up to date.
134
+
135
+ ## CI/CD
136
+
137
+ This project includes GitHub Actions workflows for automated checks and deployment.
138
+
139
+ ### Pull Request Checks
140
+
141
+ Every pull request runs typecheck and lint automatically. See `.github/workflows/ci.yml`.
142
+
143
+ ### Deployment
144
+
145
+ Pushes to `main` automatically deploy to Cloud Run. To set this up:
146
+
147
+ 1. **Run initial deploy locally** (provisions Turso database + first Cloud Run deploy):
148
+ ```bash
149
+ fluid deploy
150
+ ```
151
+
152
+ 2. **Note the database credentials** from the deploy output (DATABASE_URL and DATABASE_AUTH_TOKEN)
153
+
154
+ 3. **Add GitHub repository secrets** (Settings > Secrets and variables > Actions):
155
+ - `GCP_SA_JSON` — GCP service account key JSON ([create one](https://cloud.google.com/iam/docs/creating-managing-service-account-keys))
156
+ - `DATABASE_URL` — Turso database URL from step 1
157
+ - `DATABASE_AUTH_TOKEN` — Turso auth token from step 1
158
+
159
+ 4. **Add GitHub repository variables** (Settings > Secrets and variables > Actions > Variables):
160
+ - `GCP_PROJECT` — Your GCP project ID
161
+ - `CLOUD_RUN_REGION` — Cloud Run region (default: `us-central1`)
162
+
163
+ 5. **Push to main** — deployment happens automatically
164
+
165
+ ## Styling
166
+
167
+ This project uses [Tailwind CSS 4](https://tailwindcss.com/) for styling. The global styles are imported in `src/index.css`. You can customize the theme and add utilities there.
168
+
169
+ ## Learn More
170
+
171
+ - [Fluid Portal SDK Documentation](https://github.com/fluidcommerce)
172
+ - [Hono Documentation](https://hono.dev/)
173
+ - [Drizzle ORM Documentation](https://orm.drizzle.team/)
174
+ - [Vite Documentation](https://vite.dev/)
175
+ - [React Documentation](https://react.dev/)
176
+ - [Tailwind CSS Documentation](https://tailwindcss.com/)
@@ -0,0 +1,7 @@
1
+ CREATE TABLE IF NOT EXISTS `todos` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `title` text NOT NULL,
4
+ `completed` integer DEFAULT false NOT NULL,
5
+ `created_at` text DEFAULT (current_timestamp) NOT NULL,
6
+ `updated_at` text DEFAULT (current_timestamp) NOT NULL
7
+ );
@@ -0,0 +1,63 @@
1
+ {
2
+ "id": "00000000-0000-0000-0000-000000000000",
3
+ "prevId": "",
4
+ "version": "6",
5
+ "dialect": "sqlite",
6
+ "tables": {
7
+ "todos": {
8
+ "name": "todos",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "integer",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": true
16
+ },
17
+ "title": {
18
+ "name": "title",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "completed": {
25
+ "name": "completed",
26
+ "type": "integer",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false,
30
+ "default": false
31
+ },
32
+ "created_at": {
33
+ "name": "created_at",
34
+ "type": "text",
35
+ "primaryKey": false,
36
+ "notNull": true,
37
+ "autoincrement": false,
38
+ "default": "(current_timestamp)"
39
+ },
40
+ "updated_at": {
41
+ "name": "updated_at",
42
+ "type": "text",
43
+ "primaryKey": false,
44
+ "notNull": true,
45
+ "autoincrement": false,
46
+ "default": "(current_timestamp)"
47
+ }
48
+ },
49
+ "indexes": {},
50
+ "foreignKeys": {},
51
+ "compositePrimaryKeys": {},
52
+ "uniqueConstraints": {},
53
+ "checkConstraints": {}
54
+ }
55
+ },
56
+ "enums": {},
57
+ "views": {},
58
+ "_meta": {
59
+ "schemas": {},
60
+ "tables": {},
61
+ "columns": {}
62
+ }
63
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1737000000000,
9
+ "tag": "0000_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ const url = process.env.DATABASE_URL ?? "file:local.db";
4
+ const isLocal = url.startsWith("file:") || url === ":memory:";
5
+
6
+ export default defineConfig({
7
+ schema: "./src/server/db/schema.ts",
8
+ out: "./drizzle",
9
+ dialect: isLocal ? "sqlite" : "turso",
10
+ dbCredentials: isLocal
11
+ ? { url }
12
+ : { url, authToken: process.env.DATABASE_AUTH_TOKEN },
13
+ });
@@ -0,0 +1,14 @@
1
+ import { build } from "esbuild";
2
+
3
+ await build({
4
+ entryPoints: ["src/server/entry.ts"],
5
+ bundle: true,
6
+ platform: "node",
7
+ target: "node20",
8
+ format: "esm",
9
+ outfile: "dist/server/entry.js",
10
+ external: ["@libsql/client", "drizzle-orm"],
11
+ banner: {
12
+ js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);',
13
+ },
14
+ });
@@ -0,0 +1,13 @@
1
+ import js from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+
5
+ export default tseslint.config(
6
+ js.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ {
9
+ plugins: { "react-hooks": reactHooks },
10
+ rules: reactHooks.configs.recommended.rules,
11
+ },
12
+ { ignores: ["dist/", "*.config.*"] },
13
+ );
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build && node esbuild.config.js",
9
+ "start": "node dist/server/entry.js",
10
+ "preview": "vite preview",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint src/",
13
+ "db:generate": "drizzle-kit generate",
14
+ "db:migrate": "drizzle-kit migrate",
15
+ "db:push": "drizzle-kit push",
16
+ "db:studio": "drizzle-kit studio",
17
+ "deploy": "fluid deploy",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest"
20
+ },
21
+ "dependencies": {
22
+ "@fluid-app/portal-sdk": "{{sdkVersion}}",
23
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
24
+ "@fortawesome/pro-regular-svg-icons": "^6.7.2",
25
+ "@fortawesome/react-fontawesome": "^0.2.2",
26
+ "@tanstack/react-query": "^5.90.0",
27
+ "@hono/node-server": "^1.13.0",
28
+ "@libsql/client": "^0.17.0",
29
+ "drizzle-orm": "^0.44.0",
30
+ "hono": "^4.7.0",
31
+ "react": "^19.0.0",
32
+ "react-dom": "^19.0.0",
33
+ "react-hook-form": "^7.55.0",
34
+ "@hookform/resolvers": "^4.1.3",
35
+ "zod": "^3.24.0"
36
+ },
37
+ "devDependencies": {
38
+ "@fluid-app/fluid-cli-portal": "{{sdkVersion}}",
39
+ "@eslint/js": "^9.0.0",
40
+ "@hono/vite-dev-server": "^0.25.0",
41
+ "@tailwindcss/vite": "^4.0.0",
42
+ "esbuild": "^0.25.0",
43
+ "@types/node": "^22.0.0",
44
+ "@types/react": "^19.0.0",
45
+ "@types/react-dom": "^19.0.0",
46
+ "@vitejs/plugin-react": "^4.3.0",
47
+ "drizzle-kit": "^0.31.0",
48
+ "eslint": "^9.0.0",
49
+ "eslint-plugin-react-hooks": "^5.0.0",
50
+ "tailwindcss": "^4.0.0",
51
+ "typescript": "^5.6.0",
52
+ "typescript-eslint": "^8.0.0",
53
+ "vite": "^6.0.0",
54
+ "vitest": "^4.0.0"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "packageManager": "pnpm@10.4.1",
60
+ "pnpm": {
61
+ "onlyBuiltDependencies": ["esbuild"]
62
+ }
63
+ }
@@ -0,0 +1,69 @@
1
+ import type { FluidSDKConfig, FluidAuthConfig } from "@fluid-app/portal-sdk";
2
+ import { getStoredToken } from "@fluid-app/portal-sdk";
3
+
4
+ /**
5
+ * Fluid SDK Configuration
6
+ *
7
+ * This file contains the configuration for connecting to your Fluid Commerce account.
8
+ * Authentication is handled by FluidAuthProvider, which automatically:
9
+ * - Extracts tokens from URL parameters (passed by parent Fluid app)
10
+ * - Stores tokens in cookies and localStorage
11
+ * - Validates token expiration
12
+ */
13
+
14
+ /**
15
+ * Authentication configuration
16
+ *
17
+ * Pattern: safety-satisfies-over-type-annotation
18
+ * Using `satisfies FluidAuthConfig` validates the object shape against the type
19
+ * WITHOUT widening the inferred types. This means:
20
+ * - TypeScript checks that all required properties are present
21
+ * - TypeScript checks that property types are compatible
22
+ * - The actual inferred types remain as specific as possible
23
+ *
24
+ * Compare to type annotation (`const authConfig: FluidAuthConfig = {...}`):
25
+ * - Type annotation widens types to match the interface exactly
26
+ * - `satisfies` preserves literal types while validating structure
27
+ */
28
+ export const authConfig = {
29
+ /**
30
+ * Enable dev-mode auth bypass for local development.
31
+ * This lets you run the portal without a real JWT token.
32
+ * Only active in Vite dev mode (pnpm dev) - automatically disabled in production builds.
33
+ */
34
+ devBypass: true,
35
+
36
+ // Auth failure defaults: SDK automatically redirects to https://auth.fluid.app
37
+ // with a redirect_url back to the current page. To customize:
38
+ // onAuthFailure: () => { window.location.href = "/custom-login"; },
39
+ // authUrl: "https://custom-auth.example.com",
40
+ } satisfies FluidAuthConfig;
41
+
42
+ /**
43
+ * API client configuration
44
+ *
45
+ * Pattern: safety-satisfies-over-type-annotation
46
+ * See authConfig above for explanation of the `satisfies` pattern.
47
+ *
48
+ * This config is passed to FluidProvider to configure the API client.
49
+ * The callbacks (getAuthToken, onAuthError) are called by the SDK
50
+ * when making API requests.
51
+ */
52
+ export const fluidConfig = {
53
+ /**
54
+ * Base URL for the Fluid API
55
+ * Change this to your Fluid instance URL if self-hosted
56
+ */
57
+ baseUrl: import.meta.env.VITE_API_URL ?? "https://api.fluid.app",
58
+
59
+ /**
60
+ * Returns the authentication token for API requests
61
+ * This reads from the token stored by FluidAuthProvider
62
+ */
63
+ getAuthToken: () => {
64
+ return getStoredToken() ?? null;
65
+ },
66
+
67
+ // 401 errors default: SDK automatically redirects to https://auth.fluid.app
68
+ // To customize: onAuthError: () => { clearTokens(); window.location.reload(); },
69
+ } satisfies FluidSDKConfig;
@@ -0,0 +1,10 @@
1
+ import { createClient } from "@libsql/client";
2
+ import { drizzle } from "drizzle-orm/libsql";
3
+ import * as schema from "./schema";
4
+
5
+ const client = createClient({
6
+ url: process.env.DATABASE_URL ?? "file:local.db",
7
+ authToken: process.env.DATABASE_AUTH_TOKEN,
8
+ });
9
+
10
+ export const db = drizzle(client, { schema });
@@ -0,0 +1,12 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { migrate } from "drizzle-orm/libsql/migrator";
4
+ import { db } from "./index";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export async function runMigrations() {
9
+ await migrate(db, {
10
+ migrationsFolder: resolve(__dirname, "../../../drizzle"),
11
+ });
12
+ }
@@ -0,0 +1,14 @@
1
+ import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
2
+ import { sql } from "drizzle-orm";
3
+
4
+ export const todos = sqliteTable("todos", {
5
+ id: integer("id").primaryKey({ autoIncrement: true }),
6
+ title: text("title").notNull(),
7
+ completed: integer("completed", { mode: "boolean" }).notNull().default(false),
8
+ createdAt: text("created_at")
9
+ .notNull()
10
+ .default(sql`(current_timestamp)`),
11
+ updatedAt: text("updated_at")
12
+ .notNull()
13
+ .default(sql`(current_timestamp)`),
14
+ });
@@ -0,0 +1,59 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { serveStatic } from "@hono/node-server/serve-static";
3
+ import { runMigrations } from "./db/migrate";
4
+ import app from "./index";
5
+
6
+ // Run database migrations before starting the server
7
+ try {
8
+ await runMigrations();
9
+ } catch (error) {
10
+ console.error("Failed to run database migrations:", error);
11
+ console.error(
12
+ "Ensure DATABASE_URL and DATABASE_AUTH_TOKEN are set correctly.",
13
+ );
14
+ process.exit(1);
15
+ }
16
+
17
+ // Cache immutable hashed assets for 1 year
18
+ app.use("/assets/*", async (c, next) => {
19
+ await next();
20
+ c.header("Cache-Control", "public, max-age=31536000, immutable");
21
+ });
22
+
23
+ // Serve static frontend assets from dist/public/
24
+ app.use("/*", serveStatic({ root: "./dist/public" }));
25
+
26
+ // SPA fallback — serve index.html for all unmatched routes
27
+ app.get("*", serveStatic({ root: "./dist/public", path: "index.html" }));
28
+
29
+ const port = Number(process.env.PORT) || 8080;
30
+
31
+ const server = serve({ fetch: app.fetch, port }, (info) => {
32
+ console.log(`Server listening on http://localhost:${info.port}`);
33
+ });
34
+
35
+ server.on("error", (err: NodeJS.ErrnoException) => {
36
+ if (err.code === "EADDRINUSE") {
37
+ console.error(`\nPort ${port} is already in use.`);
38
+ console.error(
39
+ `Try: PORT=${port === 8080 ? 3001 : port + 1} node dist/server/entry.js\n`,
40
+ );
41
+ process.exit(1);
42
+ }
43
+ throw err;
44
+ });
45
+
46
+ // Graceful shutdown
47
+ function shutdown() {
48
+ console.log("Shutting down gracefully...");
49
+ server.close();
50
+ const forceExit = setTimeout(() => {
51
+ console.error("Forcing shutdown after timeout");
52
+ process.exit(1);
53
+ }, 10_000);
54
+ // Allow the process to exit naturally if close finishes before timeout
55
+ forceExit.unref();
56
+ }
57
+
58
+ process.on("SIGTERM", shutdown);
59
+ process.on("SIGINT", shutdown);
@@ -0,0 +1,33 @@
1
+ import { Hono } from "hono";
2
+ import { secureHeaders } from "hono/secure-headers";
3
+ import { cors } from "hono/cors";
4
+ import { logger } from "hono/logger";
5
+ import { apiRoutes } from "./routes/index";
6
+
7
+ const app = new Hono();
8
+
9
+ // Security middleware
10
+ app.use("*", secureHeaders());
11
+ app.use(
12
+ "/api/*",
13
+ cors({
14
+ origin: process.env.ALLOWED_ORIGIN ?? "http://localhost:5173",
15
+ }),
16
+ );
17
+
18
+ // Logging
19
+ app.use("*", logger());
20
+
21
+ // Global error handler
22
+ app.onError((err, c) => {
23
+ console.error(err);
24
+ return c.json({ error: "Internal server error" }, 500);
25
+ });
26
+
27
+ // Health check
28
+ app.get("/api/health", (c) => c.json({ status: "ok" }));
29
+
30
+ // Mount API routes
31
+ app.route("/api", apiRoutes);
32
+
33
+ export default app;