mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 05:01:17 +00:00
Merge pull request #16 from kennethnym/feat/aris-backend
feat(backend): init aris-backend with Hono
This commit is contained in:
19
apps/aris-backend/package.json
Normal file
19
apps/aris-backend/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@aris/backend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch src/index.ts",
|
||||||
|
"start": "bun run src/index.ts",
|
||||||
|
"test": "bun test src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aris/core": "workspace:*",
|
||||||
|
"@aris/source-location": "workspace:*",
|
||||||
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
|
"better-auth": "^1",
|
||||||
|
"hono": "^4",
|
||||||
|
"postgres": "^3"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/aris-backend/src/index.ts
Normal file
10
apps/aris-backend/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Hono } from "hono"
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.get("/health", (c) => c.json({ status: "ok" }))
|
||||||
|
|
||||||
|
export default {
|
||||||
|
port: 3000,
|
||||||
|
fetch: app.fetch,
|
||||||
|
}
|
||||||
7
apps/aris-backend/tsconfig.json
Normal file
7
apps/aris-backend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["bun-types"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
50
bun.lock
50
bun.lock
@@ -13,6 +13,18 @@
|
|||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"apps/aris-backend": {
|
||||||
|
"name": "@aris/backend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aris/core": "workspace:*",
|
||||||
|
"@aris/source-location": "workspace:*",
|
||||||
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
|
"better-auth": "^1",
|
||||||
|
"hono": "^4",
|
||||||
|
"postgres": "^3",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/aris-core": {
|
"packages/aris-core": {
|
||||||
"name": "@aris/core",
|
"name": "@aris/core",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -52,6 +64,8 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@aris/backend": ["@aris/backend@workspace:apps/aris-backend"],
|
||||||
|
|
||||||
"@aris/core": ["@aris/core@workspace:packages/aris-core"],
|
"@aris/core": ["@aris/core@workspace:packages/aris-core"],
|
||||||
|
|
||||||
"@aris/data-source-weatherkit": ["@aris/data-source-weatherkit@workspace:packages/aris-data-source-weatherkit"],
|
"@aris/data-source-weatherkit": ["@aris/data-source-weatherkit@workspace:packages/aris-data-source-weatherkit"],
|
||||||
@@ -66,6 +80,18 @@
|
|||||||
|
|
||||||
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
||||||
|
|
||||||
|
"@better-auth/core": ["@better-auth/core@1.4.17", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-WSaEQDdUO6B1CzAmissN6j0lx9fM9lcslEYzlApB5UzFaBeAOHNUONTdglSyUs6/idiZBoRvt0t/qMXCgIU8ug=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.17", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.17" } }, "sha512-R1BC4e/bNjQbXu7lG6ubpgmsPj7IMqky5DvMlzAtnAJWJhh99pMh/n6w5gOHa0cqDZgEAuj75IPTxv+q3YiInA=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
|
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
|
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
|
||||||
@@ -98,6 +124,8 @@
|
|||||||
|
|
||||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="],
|
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||||
@@ -106,16 +134,38 @@
|
|||||||
|
|
||||||
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.4.17", "", { "dependencies": { "@better-auth/core": "1.4.17", "@better-auth/telemetry": "1.4.17", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.10", "", {}, "sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||||
|
|
||||||
"oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="],
|
"oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="],
|
||||||
|
|
||||||
"oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="],
|
"oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="],
|
||||||
|
|
||||||
|
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="],
|
"tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
178
docs/backend-spec.md
Normal file
178
docs/backend-spec.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# ARIS Backend Specification
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
ARIS needs a backend service that manages per-user FeedEngine instances and delivers real-time feed updates to clients. The backend must handle authentication, maintain WebSocket connections for live updates, and accept context updates (like location) that trigger feed recalculations.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- Email/password authentication using BetterAuth
|
||||||
|
- PostgreSQL for session and user storage
|
||||||
|
- Session tokens validated via `Authorization: Bearer <token>` header
|
||||||
|
- Auth endpoints exposed via BetterAuth's built-in routes
|
||||||
|
|
||||||
|
### FeedEngine Management
|
||||||
|
- Each authenticated user gets their own FeedEngine instance
|
||||||
|
- Instances are cached in memory with a 30-minute TTL
|
||||||
|
- TTL resets on any activity (WebSocket message, location update)
|
||||||
|
- Default sources registered for each user: `LocationSource`, `WeatherSource`, `TflSource`
|
||||||
|
- Source configuration is hardcoded initially (customization deferred)
|
||||||
|
|
||||||
|
### WebSocket Connection
|
||||||
|
- Single endpoint: `GET /ws` (upgrades to WebSocket)
|
||||||
|
- Authentication via `Authorization: Bearer <token>` header on upgrade request
|
||||||
|
- Rejected before upgrade if token is invalid
|
||||||
|
- Multiple connections per user allowed (e.g., multiple devices)
|
||||||
|
- All connections for a user receive the same feed updates
|
||||||
|
- On connect: immediately send current feed state
|
||||||
|
|
||||||
|
### JSON-RPC Protocol
|
||||||
|
All WebSocket communication uses JSON-RPC 2.0.
|
||||||
|
|
||||||
|
**Client → Server (Requests):**
|
||||||
|
```json
|
||||||
|
{ "jsonrpc": "2.0", "method": "location.update", "params": { "lat": 51.5, "lng": -0.1, "accuracy": 10, "timestamp": "2025-01-01T12:00:00Z" }, "id": 1 }
|
||||||
|
{ "jsonrpc": "2.0", "method": "feed.refresh", "params": {}, "id": 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (Responses):**
|
||||||
|
```json
|
||||||
|
{ "jsonrpc": "2.0", "result": { "ok": true }, "id": 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (Notifications - no id):**
|
||||||
|
```json
|
||||||
|
{ "jsonrpc": "2.0", "method": "feed.update", "params": { "items": [...], "errors": [...] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON-RPC Methods
|
||||||
|
|
||||||
|
| Method | Params | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `location.update` | `{ lat, lng, accuracy, timestamp }` | Push location update, triggers feed refresh |
|
||||||
|
| `feed.refresh` | `{}` | Force manual feed refresh |
|
||||||
|
|
||||||
|
### Server Notifications
|
||||||
|
|
||||||
|
| Method | Params | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `feed.update` | `{ context, items, errors }` | Feed state changed |
|
||||||
|
| `error` | `{ code, message, data? }` | Source or system error |
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Source failures during refresh are reported via `error` notification
|
||||||
|
- Format: `{ "jsonrpc": "2.0", "method": "error", "params": { "code": -32000, "message": "...", "data": { "sourceId": "weather" } } }`
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. **Auth Flow**
|
||||||
|
- [ ] User can sign up with email/password via `POST /api/auth/sign-up`
|
||||||
|
- [ ] User can sign in via `POST /api/auth/sign-in` and receive session token
|
||||||
|
- [ ] Invalid credentials return 401
|
||||||
|
|
||||||
|
2. **WebSocket Connection**
|
||||||
|
- [ ] `GET /ws` with valid `Authorization` header upgrades to WebSocket
|
||||||
|
- [ ] `GET /ws` without valid token returns 401 (no upgrade)
|
||||||
|
- [ ] On successful connect, client receives `feed.update` notification with current state
|
||||||
|
- [ ] Multiple connections from same user all receive updates
|
||||||
|
|
||||||
|
3. **FeedEngine Lifecycle**
|
||||||
|
- [ ] First connection for a user creates FeedEngine with default sources
|
||||||
|
- [ ] Subsequent connections reuse the same FeedEngine
|
||||||
|
- [ ] FeedEngine is destroyed after 30 minutes of inactivity
|
||||||
|
- [ ] Activity (any WebSocket message) resets the TTL
|
||||||
|
|
||||||
|
4. **JSON-RPC Methods**
|
||||||
|
- [ ] `location.update` updates LocationSource and triggers feed refresh
|
||||||
|
- [ ] `feed.refresh` triggers manual refresh
|
||||||
|
- [ ] Both return `{ "ok": true }` on success
|
||||||
|
- [ ] Invalid method returns JSON-RPC error
|
||||||
|
|
||||||
|
5. **Feed Updates**
|
||||||
|
- [ ] FeedEngine subscription pushes updates to all user's WebSocket connections
|
||||||
|
- [ ] Updates include `context`, `items`, and `errors`
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
### Phase 1: Project Setup
|
||||||
|
1. Create `apps/aris-backend` with Hono
|
||||||
|
2. Configure TypeScript, add dependencies (hono, better-auth, postgres driver)
|
||||||
|
3. Set up database connection and BetterAuth
|
||||||
|
|
||||||
|
### Phase 2: Authentication
|
||||||
|
4. Configure BetterAuth with email/password provider
|
||||||
|
5. Mount BetterAuth routes at `/api/auth/*`
|
||||||
|
6. Create session validation helper for extracting user from token
|
||||||
|
|
||||||
|
### Phase 3: FeedEngine Manager
|
||||||
|
7. Create `FeedEngineManager` class:
|
||||||
|
- `getOrCreate(userId): FeedEngine` - returns cached or creates new
|
||||||
|
- `touch(userId)` - resets TTL
|
||||||
|
- `destroy(userId)` - manual cleanup
|
||||||
|
- Internal TTL cleanup loop
|
||||||
|
8. Factory function to create FeedEngine with default sources
|
||||||
|
|
||||||
|
### Phase 4: WebSocket Handler
|
||||||
|
9. Create WebSocket upgrade endpoint at `/ws`
|
||||||
|
10. Validate `Authorization` header before upgrade
|
||||||
|
11. On connect: register connection, send initial feed state
|
||||||
|
12. On disconnect: unregister connection
|
||||||
|
|
||||||
|
### Phase 5: JSON-RPC Handler
|
||||||
|
13. Create JSON-RPC message parser and dispatcher
|
||||||
|
14. Implement `location.update` method
|
||||||
|
15. Implement `feed.refresh` method
|
||||||
|
16. Wire FeedEngine subscription to broadcast `feed.update` to all user connections
|
||||||
|
|
||||||
|
### Phase 6: Connection Manager
|
||||||
|
17. Create `ConnectionManager` to track WebSocket connections per user
|
||||||
|
18. Broadcast helper to send to all connections for a user
|
||||||
|
|
||||||
|
### Phase 7: Integration & Testing
|
||||||
|
19. Integration test: auth → connect → location update → receive feed
|
||||||
|
20. Test multiple connections receive same updates
|
||||||
|
21. Test TTL cleanup
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/aris-backend/
|
||||||
|
├── package.json
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Entry point, Hono app
|
||||||
|
│ ├── auth.ts # BetterAuth configuration
|
||||||
|
│ ├── db.ts # Database connection
|
||||||
|
│ ├── ws/
|
||||||
|
│ │ ├── handler.ts # WebSocket upgrade & message handling
|
||||||
|
│ │ ├── jsonrpc.ts # JSON-RPC parser & types
|
||||||
|
│ │ └── methods.ts # Method implementations
|
||||||
|
│ ├── feed/
|
||||||
|
│ │ ├── manager.ts # FeedEngineManager (TTL cache)
|
||||||
|
│ │ ├── factory.ts # Creates FeedEngine with default sources
|
||||||
|
│ │ └── connections.ts # ConnectionManager (user → WebSocket[])
|
||||||
|
│ └── types.ts # Shared types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4",
|
||||||
|
"better-auth": "^1",
|
||||||
|
"postgres": "^3",
|
||||||
|
"@aris/core": "workspace:*",
|
||||||
|
"@aris/source-location": "workspace:*",
|
||||||
|
"@aris/source-weatherkit": "workspace:*",
|
||||||
|
"@aris/data-source-tfl": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Questions (Deferred)
|
||||||
|
|
||||||
|
- User source configuration storage (database schema)
|
||||||
|
- Rate limiting on WebSocket methods
|
||||||
|
- Reconnection handling (client-side concern)
|
||||||
|
- Horizontal scaling (would need Redis for shared state)
|
||||||
Reference in New Issue
Block a user