@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.
- package/dist/index.d.mts +680 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2230 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/templates/base/index.html +14 -0
- package/templates/base/src/App.tsx +18 -0
- package/templates/base/src/index.css +80 -0
- package/templates/base/src/main.tsx +42 -0
- package/templates/base/src/navigation.config.ts +21 -0
- package/templates/base/src/portal.config.ts +32 -0
- package/templates/base/src/screens/Dashboard.tsx +133 -0
- package/templates/base/src/screens/ExampleForm.tsx +187 -0
- package/templates/base/src/vite-env.d.ts +1 -0
- package/templates/base/tsconfig.json +26 -0
- package/templates/fullstack/.dockerignore +9 -0
- package/templates/fullstack/.env.example +15 -0
- package/templates/fullstack/.github/workflows/ci.yml +47 -0
- package/templates/fullstack/.github/workflows/deploy.yml +54 -0
- package/templates/fullstack/Dockerfile +44 -0
- package/templates/fullstack/README.md.template +176 -0
- package/templates/fullstack/drizzle/0000_initial.sql +7 -0
- package/templates/fullstack/drizzle/meta/0000_snapshot.json +63 -0
- package/templates/fullstack/drizzle/meta/_journal.json +13 -0
- package/templates/fullstack/drizzle.config.ts +13 -0
- package/templates/fullstack/esbuild.config.js +14 -0
- package/templates/fullstack/eslint.config.js +13 -0
- package/templates/fullstack/package.json.template +63 -0
- package/templates/fullstack/src/fluid.config.ts.template +69 -0
- package/templates/fullstack/src/server/db/index.ts +10 -0
- package/templates/fullstack/src/server/db/migrate.ts +12 -0
- package/templates/fullstack/src/server/db/schema.ts +14 -0
- package/templates/fullstack/src/server/entry.ts +59 -0
- package/templates/fullstack/src/server/index.ts +33 -0
- package/templates/fullstack/src/server/routes/index.test.ts +123 -0
- package/templates/fullstack/src/server/routes/index.ts +109 -0
- package/templates/fullstack/src/server/routes/schemas.ts +7 -0
- package/templates/fullstack/src/test/setup.ts +9 -0
- package/templates/fullstack/vite.config.ts +39 -0
- package/templates/fullstack/vitest.config.ts +9 -0
- package/templates/starter/.env.example +1 -0
- package/templates/starter/README.md.template +218 -0
- package/templates/starter/package.json.template +40 -0
- package/templates/starter/src/fluid.config.ts.template +69 -0
- 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
|
+
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;
|