@sansavision/create-pulse 0.4.1 → 0.4.3

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 (41) hide show
  1. package/README.md +27 -3
  2. package/dist/index.js +3 -1
  3. package/package.json +1 -1
  4. package/templates/nextjs-auth-demo/.env.example +6 -0
  5. package/templates/nextjs-auth-demo/README.md +125 -0
  6. package/templates/nextjs-auth-demo/_gitignore +33 -0
  7. package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
  8. package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
  9. package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
  10. package/templates/nextjs-auth-demo/next.config.ts +7 -0
  11. package/templates/nextjs-auth-demo/package.json +36 -0
  12. package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
  13. package/templates/nextjs-auth-demo/public/file.svg +1 -0
  14. package/templates/nextjs-auth-demo/public/globe.svg +1 -0
  15. package/templates/nextjs-auth-demo/public/next.svg +1 -0
  16. package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
  17. package/templates/nextjs-auth-demo/public/window.svg +1 -0
  18. package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
  19. package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
  20. package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
  21. package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
  22. package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
  23. package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
  24. package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
  25. package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
  26. package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
  27. package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
  28. package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
  29. package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
  30. package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
  31. package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
  32. package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
  33. package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
  34. package/templates/nextjs-auth-demo/src/lib/auth.ts +14 -0
  35. package/templates/nextjs-auth-demo/src/lib/db.ts +6 -0
  36. package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
  37. package/templates/nextjs-auth-demo/src/lib/schema.ts +107 -0
  38. package/templates/nextjs-auth-demo/tsconfig.json +34 -0
  39. package/templates/react-queue-demo/README.md +6 -7
  40. package/templates/react-queue-demo/src/App.tsx +34 -13
  41. package/src/index.ts +0 -115
package/README.md CHANGED
@@ -26,18 +26,37 @@ npx @sansavision/create-pulse my-pulse-app
26
26
 
27
27
  When you run `create-pulse`, you'll be prompted to select a template.
28
28
 
29
- ### 1. Watch Together (React + TS)
29
+ ### 1. Next.js + Auth (Full Demo)
30
+ An **investor-ready** demo application showcasing the full capabilities of Pulse.
31
+ - **Better Auth** with email/password, local SQLite + Drizzle ORM
32
+ - **Webhook auth** — Pulse relay verifies tokens via your Next.js API
33
+ - 5 comprehensive demos: Real-time Chat, Watch Together, Durable Queues (with offline simulation), Game State Sync, E2E Encrypted Chat
34
+ - Multi-user testing with separate browser tabs/incognito windows
35
+ - Built with **Next.js 16**, **React 19**, **Tailwind CSS v4**, **TypeScript**
36
+
37
+ ### 2. Watch Together (React + TS)
30
38
  A clean implementation of the "Watch Together" synchronized video player.
31
39
  - Features a custom `usePulse` hook for lifecycle management.
32
40
  - Handles syncing `play`, `pause`, and `seek` events across clients natively via the Pulse Relay.
33
41
  - Built with React, Vite, Tailwind CSS, and TypeScript.
34
42
 
35
- ### 2. All Features (React + TS)
43
+ ### 3. All Features (React + TS)
36
44
  A kitchen-sink boilerplate that integrates multiple features.
37
45
  - Perfect for exploring the SDK limits.
38
46
  - Contains stubs and route setups for Chat, Metrics, and Video sync.
39
47
  - Built with React, Vite, Tailwind CSS, and TypeScript.
40
48
 
49
+ ### 4. Durable Queues (React + TS)
50
+ Demonstrates persistent message queues with publish/consume/ack workflows.
51
+ - Supports multiple storage backends (Memory, WAL, Postgres, Redis).
52
+ - Shows offline resilience and message recovery.
53
+ - Built with React, Vite, Tailwind CSS, and TypeScript.
54
+
55
+ ### 5. Vanilla JS (Basic)
56
+ A minimal setup with zero frameworks.
57
+ - Raw WebSocket connection to the Pulse Relay.
58
+ - Good for understanding the protocol at a low level.
59
+
41
60
  ## Running Your Scaffolded App
42
61
 
43
62
  After your app is generated:
@@ -55,4 +74,9 @@ After your app is generated:
55
74
  npm run dev
56
75
  ```
57
76
 
58
- **Note:** The templates are configured by default to connect to a local Pulse Relay running at `ws://localhost:4001`. Ensure your local Rust relay is running, or update `src/App.tsx` to point to a production relay URL.
77
+ **Note:** The templates are configured by default to connect to a local Pulse Relay running at `ws://localhost:4001`. Start the server with `pulse serve` (or `pulse dev` for the full dev environment), or update your config to point to a production relay URL.
78
+
79
+ For the **Next.js + Auth** template, also start the relay with webhook auth:
80
+ ```bash
81
+ npx @sansavision/pulse-cli serve --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
82
+ ```
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ async function main() {
51
51
  return p.select({
52
52
  message: "Pick a template:",
53
53
  options: [
54
+ { value: "nextjs-auth-demo", label: "Next.js + Auth (Full Demo)", hint: "Better Auth, all features, investor-ready" },
54
55
  { value: "react-watch-together", label: "Watch Together (React + TS)", hint: "Synchronized video playback" },
55
56
  { value: "react-all-features", label: "All Features (React + TS)", hint: "Chat, Video, Audio, RPC" },
56
57
  { value: "react-queue-demo", label: "Durable Queues (React + TS)", hint: "Persistent queues with WAL/Postgres/Redis" },
@@ -101,7 +102,8 @@ function copyDir(src, dest) {
101
102
  const entries = import_fs.default.readdirSync(src, { withFileTypes: true });
102
103
  for (const entry of entries) {
103
104
  const srcPath = import_path.default.join(src, entry.name);
104
- if (entry.name === "node_modules" || entry.name === "dist") continue;
105
+ if (["node_modules", "dist", ".next", ".env", "package-lock.json"].includes(entry.name)) continue;
106
+ if (entry.name.endsWith(".db") || entry.name.endsWith(".db-journal")) continue;
105
107
  const destPath = import_path.default.join(dest, entry.name);
106
108
  if (entry.isDirectory()) {
107
109
  if (!import_fs.default.existsSync(destPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sansavision/create-pulse",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Scaffold a new Pulse application",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,6 @@
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
6
+ NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
@@ -0,0 +1,125 @@
1
+ # Pulse + Next.js Auth Demo
2
+
3
+ An **investor-ready** demo application showcasing the full capabilities of [Pulse](https://github.com/Sansa-Organisation/pulse) — the real-time protocol for modern applications.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Better Auth** — Full authentication with email/password, local SQLite + Drizzle ORM
8
+ - 💬 **Real-time Chat** — Multi-user rooms with presence and typing indicators
9
+ - 📹 **Watch Together** — Synchronized video playback across users
10
+ - 📬 **Durable Queues** — Offline simulation with message persistence
11
+ - 🎮 **Game State Sync** — Real-time tic-tac-toe with shared state
12
+ - 🔒 **E2E Encrypted Chat** — End-to-end encryption with ciphertext visualization
13
+
14
+ ## Quick Start
15
+
16
+ ### 1. Install dependencies
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ### 2. Configure environment
23
+
24
+ ```bash
25
+ cp .env.example .env
26
+ ```
27
+
28
+ Edit `.env` and set your secret (generate one with `openssl rand -base64 32`):
29
+
30
+ ```env
31
+ BETTER_AUTH_SECRET=your-generated-secret-here
32
+ BETTER_AUTH_URL=http://localhost:3000
33
+ NEXT_PUBLIC_PULSE_URL=ws://localhost:4001
34
+ ```
35
+
36
+ > **Note:** `BETTER_AUTH_URL` must match the port your app runs on. If you use `--port 3444`, set it to `http://localhost:3444`.
37
+
38
+ ### 3. Create database tables
39
+
40
+ The auth schema is pre-generated in `src/lib/schema.ts`. Push it to SQLite:
41
+
42
+ ```bash
43
+ npm run db:push
44
+ ```
45
+
46
+ > This creates the `user`, `session`, `account`, and `verification` tables in `./sqlite.db`.
47
+
48
+ ### 4. Start the Pulse relay
49
+
50
+ In a separate terminal, start the relay with webhook auth:
51
+
52
+ ```bash
53
+ npx @sansavision/pulse-cli serve \
54
+ --auth-mode webhook \
55
+ --auth-webhook http://localhost:3000/api/pulse/verify
56
+ ```
57
+
58
+ > If your app runs on a different port, update the `--auth-webhook` URL accordingly.
59
+
60
+ ### 5. Start the app
61
+
62
+ ```bash
63
+ npm run dev
64
+ ```
65
+
66
+ Open [http://localhost:3000](http://localhost:3000) in your browser.
67
+
68
+ ## Multi-User Testing
69
+
70
+ 1. Open `http://localhost:3000` in a browser
71
+ 2. Create an account and sign in
72
+ 3. Open a second browser tab (or incognito window)
73
+ 4. Create a different account
74
+ 5. Both users can now interact in real-time across all demos
75
+
76
+ ## Architecture
77
+
78
+ ```
79
+ Next.js App (port 3000) Pulse Relay (port 4001)
80
+ ├── /api/auth/[...all] ◄──────┐ ├── WebSocket gateway
81
+ ├── /api/pulse/verify ◄──────┼──── ├── Auth webhook verification
82
+ ├── / (Landing) │ └── QUIC streams
83
+ ├── /auth/sign-in │
84
+ ├── /auth/sign-up │
85
+ └── /dashboard (protected) │
86
+ ├── /demos/chat │
87
+ ├── /demos/watch-together │
88
+ ├── /demos/queues ─┘
89
+ ├── /demos/game-sync
90
+ └── /demos/encrypted-chat
91
+ ```
92
+
93
+ ## Technologies
94
+
95
+ - **Next.js 16** + **React 19** + **Tailwind CSS v4**
96
+ - **Better Auth** with **bearer plugin**
97
+ - **SQLite** + **Drizzle ORM**
98
+ - **Pulse SDK** (`@sansavision/pulse-sdk`)
99
+
100
+ ## Environment Variables
101
+
102
+ | Variable | Description | Default |
103
+ |----------|-------------|---------|
104
+ | `BETTER_AUTH_SECRET` | Auth encryption secret (32+ chars) | — |
105
+ | `BETTER_AUTH_URL` | Base URL of the app (must match your port) | `http://localhost:3000` |
106
+ | `NEXT_PUBLIC_PULSE_URL` | Pulse relay WebSocket URL | `ws://localhost:4001` |
107
+
108
+ ## Troubleshooting
109
+
110
+ ### "Invalid origin" error on signup
111
+
112
+ Your `BETTER_AUTH_URL` doesn't match the port the app is running on. For example, if you start the app with `--port 3444`, set `BETTER_AUTH_URL=http://localhost:3444` in `.env`.
113
+
114
+ ### "Connecting to Pulse relay..." stays loading
115
+
116
+ Make sure the Pulse relay is running in a separate terminal with `--auth-mode webhook`. Without auth flags, the relay won't accept authenticated connections.
117
+
118
+ ### Database errors
119
+
120
+ If you see table-related errors, re-run the schema push:
121
+
122
+ ```bash
123
+ rm -f sqlite.db
124
+ npm run db:push
125
+ ```
@@ -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/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"],
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "pulse-nextjs-auth-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
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
+ "next": "16.1.6",
16
+ "react": "19.2.3",
17
+ "react-dom": "19.2.3",
18
+ "better-auth": "^1.2.0",
19
+ "better-sqlite3": "^12.0.0",
20
+ "drizzle-orm": "^0.41.0",
21
+ "@sansavision/pulse-sdk": "^0.4.2",
22
+ "lucide-react": "^0.412.0"
23
+ },
24
+ "devDependencies": {
25
+ "@tailwindcss/postcss": "^4",
26
+ "@types/node": "^20",
27
+ "@types/react": "^19",
28
+ "@types/react-dom": "^19",
29
+ "@types/better-sqlite3": "^7.6.0",
30
+ "drizzle-kit": "^0.31.0",
31
+ "eslint": "^9",
32
+ "eslint-config-next": "16.1.6",
33
+ "tailwindcss": "^4",
34
+ "typescript": "^5"
35
+ }
36
+ }
@@ -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,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
+ }