@sansavision/create-pulse 0.4.4 → 0.4.6

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 (97) hide show
  1. package/dist/index.js +2 -0
  2. package/package.json +2 -2
  3. package/templates/aurora-auth-node-demo/README.md +43 -0
  4. package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
  5. package/templates/aurora-auth-node-demo/bun.lock +679 -0
  6. package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
  7. package/templates/aurora-auth-node-demo/package.json +39 -0
  8. package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
  9. package/templates/aurora-auth-node-demo/server.mjs +46 -0
  10. package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
  11. package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
  12. package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
  13. package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
  14. package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
  15. package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
  16. package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
  17. package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
  18. package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
  19. package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
  20. package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
  21. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
  22. package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
  23. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
  24. package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
  25. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
  26. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
  27. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
  28. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
  29. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
  30. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
  31. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
  32. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
  33. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
  34. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
  35. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
  36. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
  37. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
  38. package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
  39. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
  40. package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
  41. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
  42. package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
  43. package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
  44. package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
  45. package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
  46. package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
  47. package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
  48. package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
  49. package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
  50. package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
  51. package/templates/nextjs-auth-demo/package.json +8 -7
  52. package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
  53. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
  54. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
  55. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
  56. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
  57. package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
  58. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
  59. package/templates/nextjs-auth-node-demo/.env.example +10 -0
  60. package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
  61. package/templates/nextjs-auth-node-demo/README.md +159 -0
  62. package/templates/nextjs-auth-node-demo/_gitignore +33 -0
  63. package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
  64. package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
  65. package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
  66. package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
  67. package/templates/nextjs-auth-node-demo/package.json +38 -0
  68. package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
  69. package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
  70. package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
  71. package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
  72. package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
  73. package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
  74. package/templates/nextjs-auth-node-demo/server.mjs +45 -0
  75. package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
  76. package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
  77. package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
  78. package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
  79. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
  80. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
  81. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
  82. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
  83. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
  84. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
  85. package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
  86. package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
  87. package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
  88. package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
  89. package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
  90. package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
  91. package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
  92. package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
  93. package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
  94. package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
  95. package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
  96. package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
  97. package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
@@ -0,0 +1,10 @@
1
+ # Better Auth
2
+ BETTER_AUTH_SECRET=replace-me-with-a-32-char-secret-key
3
+ BETTER_AUTH_URL=http://localhost:3000
4
+
5
+ # Pulse Relay (embedded — started automatically by server.mjs)
6
+ NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
7
+
8
+ # Optional: override ports
9
+ # PORT=3000
10
+ # PULSE_PORT=4001
@@ -0,0 +1,19 @@
1
+ FROM node:22-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package*.json ./
7
+ RUN npm ci --production
8
+
9
+ # Copy source
10
+ COPY . .
11
+
12
+ # Build Next.js
13
+ RUN npm run build
14
+
15
+ # Expose app + embedded relay
16
+ EXPOSE 3000 4001
17
+
18
+ # Start the unified server
19
+ CMD ["npm", "start"]
@@ -0,0 +1,159 @@
1
+ # Pulse + Next.js Auth Demo (Embedded Node.js)
2
+
3
+ A **full-featured** demo showcasing [Pulse](https://github.com/Sansa-Organisation/pulse) with the relay engine **embedded directly inside your Node.js process** using `@sansavision/pulse-node`. No separate relay process needed — everything runs as a single `node server.mjs`.
4
+
5
+ ## Features
6
+
7
+ - ⚡ **Embedded Relay** — Pulse engine runs in-process via native Rust bindings (napi-rs)
8
+ - 🔐 **Better Auth** — Full authentication with email/password, local SQLite + Drizzle ORM
9
+ - 💬 **Real-time Chat** — Multi-user rooms with presence and typing indicators
10
+ - 📹 **Watch Together** — Synchronized video playback across users
11
+ - 📬 **Durable Queues** — Offline simulation with message persistence
12
+ - 🎮 **Game State Sync** — Real-time tic-tac-toe with shared state
13
+ - 🔒 **E2E Encrypted Chat** — End-to-end encryption with ciphertext visualization
14
+ - 🏟️ **Arena Game** — Real-time multiplayer arena
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Install dependencies
19
+
20
+ ```bash
21
+ npm install
22
+ ```
23
+
24
+ ### 2. Configure environment
25
+
26
+ ```bash
27
+ cp .env.example .env
28
+ ```
29
+
30
+ Edit `.env` and set your secret (generate one with `openssl rand -base64 32`):
31
+
32
+ ```env
33
+ BETTER_AUTH_SECRET=your-generated-secret-here
34
+ BETTER_AUTH_URL=http://localhost:3000
35
+ NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
36
+ ```
37
+
38
+ ### 3. Create database tables
39
+
40
+ ```bash
41
+ npm run db:push
42
+ ```
43
+
44
+ ### 4. Start the app
45
+
46
+ ```bash
47
+ npm run dev
48
+ ```
49
+
50
+ That's it! **No separate relay process needed.** The embedded Pulse engine starts automatically alongside Next.js.
51
+
52
+ ```
53
+ ⚡ Pulse Relay running on port 4001
54
+ Region: local | Capacity: 100000
55
+ 🌐 Next.js ready on http://localhost:3000
56
+ Pulse relay (embedded) on ws://localhost:4001
57
+ ```
58
+
59
+ Open [http://localhost:3000](http://localhost:3000) in your browser.
60
+
61
+ ## How It Works
62
+
63
+ Unlike the standard `nextjs-auth-demo` which requires running a separate Pulse relay, this template uses `@sansavision/pulse-node` — native Rust bindings via napi-rs — to embed the full relay engine directly inside the Node.js process.
64
+
65
+ The `server.mjs` file:
66
+ 1. Starts the Pulse relay on a background Tokio thread pool (non-blocking)
67
+ 2. Prepares and serves the Next.js app via a custom HTTP server
68
+ 3. Handles graceful shutdown on SIGTERM/SIGINT
69
+
70
+ ## Multi-User Testing
71
+
72
+ 1. Open `http://localhost:3000` in a browser
73
+ 2. Create an account and sign in
74
+ 3. Open a second browser tab (or incognito window)
75
+ 4. Create a different account
76
+ 5. Both users can now interact in real-time across all demos
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ Single Node.js Process (server.mjs)
82
+ ├── Pulse Relay (embedded, port 4001)
83
+ │ ├── WebSocket gateway
84
+ │ ├── SFU (Selective Forwarding Unit)
85
+ │ ├── QUIC transport
86
+ │ └── WASM media pipeline
87
+ └── Next.js App (port 3000)
88
+ ├── /api/auth/[...all]
89
+ ├── /api/pulse/verify
90
+ ├── / (Landing)
91
+ ├── /auth/sign-in
92
+ ├── /auth/sign-up
93
+ └── /dashboard (protected)
94
+ ├── /demos/chat
95
+ ├── /demos/watch-together
96
+ ├── /demos/queues
97
+ ├── /demos/game-sync
98
+ ├── /demos/arena-game
99
+ └── /demos/encrypted-chat
100
+ ```
101
+
102
+ ## Technologies
103
+
104
+ - **Next.js 16** + **React 19** + **Tailwind CSS v4**
105
+ - **Better Auth** with **bearer plugin**
106
+ - **SQLite** + **Drizzle ORM**
107
+ - **Pulse SDK** (`@sansavision/pulse-sdk`) — client-side
108
+ - **Pulse Node** (`@sansavision/pulse-node`) — embedded relay
109
+
110
+ ## Environment Variables
111
+
112
+ | Variable | Description | Default |
113
+ |----------|-------------|---------|
114
+ | `BETTER_AUTH_SECRET` | Auth encryption secret (32+ chars) | — |
115
+ | `BETTER_AUTH_URL` | Base URL of the app | `http://localhost:3000` |
116
+ | `NEXT_PUBLIC_PULSE_URL` | Pulse relay WebSocket URL | `ws://localhost:4001` |
117
+ | `PORT` | Next.js server port | `3000` |
118
+ | `PULSE_PORT` | Embedded relay port | `4001` |
119
+
120
+ ## Deploying to Production
121
+
122
+ The embedded relay runs on background Tokio threads. Deploy just like any Node.js app:
123
+
124
+ ```dockerfile
125
+ FROM node:22-slim
126
+ WORKDIR /app
127
+ COPY package*.json ./
128
+ RUN npm ci --production
129
+ COPY . .
130
+ RUN npm run build
131
+ EXPOSE 3000 4001
132
+ CMD ["npm", "start"]
133
+ ```
134
+
135
+ > **Note:** `@sansavision/pulse-node` includes prebuilt native binaries for
136
+ > `darwin-arm64`, `darwin-x64`, `linux-x64-gnu`, `linux-arm64-gnu`, and `win32-x64-msvc`.
137
+ > No Rust toolchain is needed at deploy time.
138
+
139
+ ## Comparison: Standard vs Embedded
140
+
141
+ | | Standard (`nextjs-auth-demo`) | Embedded (`nextjs-auth-node-demo`) |
142
+ |---|---|---|
143
+ | Relay | Separate process | In-process (napi-rs) |
144
+ | Setup | 2 terminals | 1 command |
145
+ | Deploy | Sidecar or Docker Compose | Single container |
146
+ | Use case | Multi-service, scaling relay independently | Monolith, simplicity, edge |
147
+
148
+ ## Troubleshooting
149
+
150
+ ### "Invalid origin" error on signup
151
+
152
+ Your `BETTER_AUTH_URL` doesn't match the port. Set it to match your `PORT` env.
153
+
154
+ ### Database errors
155
+
156
+ ```bash
157
+ rm -f sqlite.db
158
+ npm run db:push
159
+ ```
@@ -0,0 +1,33 @@
1
+ # dependencies
2
+ node_modules/
3
+ .pnp
4
+ .pnp.js
5
+
6
+ # builds
7
+ .next/
8
+ out/
9
+
10
+ # database
11
+ *.db
12
+ *.db-journal
13
+
14
+ # misc
15
+ .DS_Store
16
+ *.pem
17
+
18
+ # debug
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+ .pnpm-debug.log*
23
+
24
+ # env files
25
+ .env
26
+ .env*.local
27
+
28
+ # vercel
29
+ .vercel
30
+
31
+ # typescript
32
+ *.tsbuildinfo
33
+ next-env.d.ts
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/lib/schema.ts",
5
+ out: "./drizzle",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ url: "./sqlite.db",
9
+ },
10
+ });
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ serverExternalPackages: ["better-sqlite3", "@sansavision/pulse-node"],
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "pulse-nextjs-auth-node-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "NODE_ENV=development node server.mjs",
7
+ "build": "next build",
8
+ "start": "NODE_ENV=production node server.mjs",
9
+ "db:push": "npx drizzle-kit push",
10
+ "db:generate": "npx drizzle-kit generate",
11
+ "db:migrate": "npx drizzle-kit migrate",
12
+ "db:studio": "npx drizzle-kit studio"
13
+ },
14
+ "dependencies": {
15
+ "@sansavision/pulse-node": "file:../../../pulse-node",
16
+ "@sansavision/pulse-sdk": "^0.4.2",
17
+ "better-auth": "^1.2.0",
18
+ "better-sqlite3": "^12.0.0",
19
+ "drizzle-orm": "^0.41.0",
20
+ "lucide-react": "^0.412.0",
21
+ "next": "16.1.6",
22
+ "react": "19.2.3",
23
+ "react-dom": "19.2.3",
24
+ "react-player": "^2.16.0"
25
+ },
26
+ "devDependencies": {
27
+ "@tailwindcss/postcss": "^4",
28
+ "@types/better-sqlite3": "^7.6.0",
29
+ "@types/node": "^20",
30
+ "@types/react": "^19",
31
+ "@types/react-dom": "^19",
32
+ "drizzle-kit": "^0.31.0",
33
+ "eslint": "^9",
34
+ "eslint-config-next": "16.1.6",
35
+ "tailwindcss": "^4",
36
+ "typescript": "^5"
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,45 @@
1
+ import { createServer } from "http";
2
+ import { parse } from "url";
3
+ import next from "next";
4
+ import { PulseRelay } from "@sansavision/pulse-node";
5
+
6
+ const dev = process.env.NODE_ENV !== "production";
7
+ const hostname = process.env.HOST || "localhost";
8
+ const port = parseInt(process.env.PORT || "3000", 10);
9
+ const pulsePort = parseInt(process.env.PULSE_PORT || "4001", 10);
10
+
11
+ // ── Start the embedded Pulse relay ──────────────────────────────────────────
12
+ const relay = new PulseRelay({
13
+ bind: `0.0.0.0:${pulsePort}`,
14
+ enableWebsocket: true,
15
+ });
16
+
17
+ const info = await relay.start();
18
+ console.log(`⚡ Pulse Relay running on port ${info.wsPort || info.quicPort}`);
19
+ console.log(` Region: ${info.region} | Capacity: ${info.capacity}`);
20
+
21
+ // ── Start Next.js ───────────────────────────────────────────────────────────
22
+ const app = next({ dev, hostname, port });
23
+ const handle = app.getRequestHandler();
24
+
25
+ await app.prepare();
26
+
27
+ createServer((req, res) => {
28
+ const parsedUrl = parse(req.url, true);
29
+ handle(req, res, parsedUrl);
30
+ }).listen(port, () => {
31
+ console.log(`🌐 Next.js ready on http://${hostname}:${port}`);
32
+ console.log(` Pulse relay (embedded) on ws://${hostname}:${pulsePort}`);
33
+ console.log("");
34
+ console.log(" Open in your browser to get started!");
35
+ });
36
+
37
+ // ── Graceful shutdown ───────────────────────────────────────────────────────
38
+ const shutdown = async () => {
39
+ console.log("\n🛑 Shutting down...");
40
+ await relay.stop();
41
+ process.exit(0);
42
+ };
43
+
44
+ process.on("SIGTERM", shutdown);
45
+ process.on("SIGINT", shutdown);
@@ -0,0 +1,4 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { toNextJsHandler } from "better-auth/next-js";
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,54 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ /**
5
+ * Pulse Relay Auth Webhook
6
+ *
7
+ * The Pulse relay calls this endpoint during the CONNECT handshake
8
+ * to verify the client's auth token. We use Better Auth's session
9
+ * API to validate the bearer token and return user identity.
10
+ *
11
+ * Relay config: --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
12
+ */
13
+ export async function POST(req: NextRequest) {
14
+ try {
15
+ const body = await req.json();
16
+ const { token } = body;
17
+
18
+ if (!token) {
19
+ return NextResponse.json(
20
+ { allow: false, reason: "No token provided" },
21
+ { status: 200 }
22
+ );
23
+ }
24
+
25
+ // Verify the bearer token against Better Auth's session store
26
+ const session = await auth.api.getSession({
27
+ headers: new Headers({
28
+ Authorization: `Bearer ${token}`,
29
+ }),
30
+ });
31
+
32
+ if (!session?.user) {
33
+ return NextResponse.json(
34
+ { allow: false, reason: "Invalid or expired session token" },
35
+ { status: 200 }
36
+ );
37
+ }
38
+
39
+ return NextResponse.json({
40
+ allow: true,
41
+ user_id: session.user.id,
42
+ claims: {
43
+ email: session.user.email || "",
44
+ name: session.user.name || "",
45
+ },
46
+ });
47
+ } catch (error) {
48
+ console.error("[Pulse Verify] Error:", error);
49
+ return NextResponse.json(
50
+ { allow: false, reason: "Internal verification error" },
51
+ { status: 200 }
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { signIn } from "@/lib/auth-client";
7
+ import { Zap, Mail, Lock, ArrowRight, Loader2 } from "lucide-react";
8
+
9
+ export default function SignInPage() {
10
+ const router = useRouter();
11
+ const [email, setEmail] = useState("");
12
+ const [password, setPassword] = useState("");
13
+ const [error, setError] = useState("");
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ async function handleSubmit(e: React.FormEvent) {
17
+ e.preventDefault();
18
+ setError("");
19
+ setLoading(true);
20
+
21
+ try {
22
+ const result = await signIn.email({
23
+ email,
24
+ password,
25
+ });
26
+
27
+ if (result.error) {
28
+ setError(result.error.message || "Sign in failed");
29
+ } else {
30
+ router.push("/dashboard");
31
+ }
32
+ } catch {
33
+ setError("An unexpected error occurred");
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }
38
+
39
+ return (
40
+ <div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
41
+ {/* Background effects */}
42
+ <div className="absolute top-1/4 left-1/3 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px]" />
43
+ <div className="absolute bottom-1/4 right-1/3 w-80 h-80 bg-cyan-500/15 rounded-full blur-[100px]" />
44
+
45
+ <div className="w-full max-w-md relative z-10">
46
+ {/* Logo */}
47
+ <div className="text-center mb-8">
48
+ <Link href="/" className="inline-flex items-center gap-2 mb-6">
49
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
50
+ <Zap className="w-6 h-6 text-white" />
51
+ </div>
52
+ <span className="text-2xl font-bold">Pulse</span>
53
+ </Link>
54
+ <h1 className="text-2xl font-bold mb-2">Welcome back</h1>
55
+ <p className="text-slate-400">Sign in to access the demo dashboard</p>
56
+ </div>
57
+
58
+ {/* Form */}
59
+ <form
60
+ onSubmit={handleSubmit}
61
+ className="glass rounded-2xl p-8 space-y-5"
62
+ >
63
+ {error && (
64
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
65
+ {error}
66
+ </div>
67
+ )}
68
+
69
+ <div>
70
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
71
+ Email
72
+ </label>
73
+ <div className="relative">
74
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
75
+ <input
76
+ type="email"
77
+ value={email}
78
+ onChange={(e) => setEmail(e.target.value)}
79
+ placeholder="you@example.com"
80
+ required
81
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
82
+ />
83
+ </div>
84
+ </div>
85
+
86
+ <div>
87
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">
88
+ Password
89
+ </label>
90
+ <div className="relative">
91
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
92
+ <input
93
+ type="password"
94
+ value={password}
95
+ onChange={(e) => setPassword(e.target.value)}
96
+ placeholder="••••••••"
97
+ required
98
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
99
+ />
100
+ </div>
101
+ </div>
102
+
103
+ <button
104
+ type="submit"
105
+ disabled={loading}
106
+ className="w-full py-2.5 bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 rounded-lg font-semibold transition-all hover:shadow-lg hover:shadow-purple-500/25 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
107
+ >
108
+ {loading ? (
109
+ <Loader2 className="w-4 h-4 animate-spin" />
110
+ ) : (
111
+ <>
112
+ Sign In
113
+ <ArrowRight className="w-4 h-4" />
114
+ </>
115
+ )}
116
+ </button>
117
+
118
+ <p className="text-center text-sm text-slate-400">
119
+ Don&apos;t have an account?{" "}
120
+ <Link
121
+ href="/auth/sign-up"
122
+ className="text-purple-400 hover:text-purple-300 font-medium"
123
+ >
124
+ Create one
125
+ </Link>
126
+ </p>
127
+ </form>
128
+ </div>
129
+ </div>
130
+ );
131
+ }