diff --git a/.agent/rules/project-rules.md b/.agent/rules/project-rules.md new file mode 100644 index 0000000..8e81fe5 --- /dev/null +++ b/.agent/rules/project-rules.md @@ -0,0 +1,67 @@ +--- +trigger: always_on +--- + +## Architecture & Priorities + +- This is a PoC: prioritize developer speed and clarity over abstraction. +- Treat all backends as APIs; do not couple services together. +- The AI service is stateless and must not call application APIs directly. +- always use pnpm and pnpx for package management + + +## Monorepo Structure + +- This is a TurboRepo monorepo with pnpm workspaces +- Apps live in `apps/` (currently only tanstack-start) +- Shared packages live in `packages/` +- External services live in `services/` +- Use `turbo.json` for task definitions and caching + +## Environment & Configuration + +- Use Zod for environment validation in TypeScript packages +- Follow the pattern in `packages/auth/env.ts` for type-safe env vars +- Never commit sensitive values; use `example.env` as a template +- Prefix AI service env vars with `AI_` to avoid conflicts + +## Authentication + +- Use [WorkOS](https://workos.com/docs/llms.txt) for authentication and organization context. +- Do not invent custom auth flows. +- User identity and org context must be passed explicitly to services. + +## Frontend & UI + +- All reusable UI components must live in the `packages/ui` package. use them +- Use Tailwind CSS for styling. +- Prefer shadcn/ui components where applicable. +- Do not create ad-hoc UI components in app folders. + +## Forms & State +- Here are the Tanstack [docs](https://tanstack.com/llms.txt) +- Use `@tanstack/react-form` for all forms. +- Use `@tanstack/react-query` for data fetching, caching, and mutations. +- Use `@tanstack/react-router` for routing. + +## APIs & Data Fetching + +- Use tRPC for type-safe application API calls (user data, metrics, app state). +- Use @hey-api/openapi-ts to generate a typed client for the FastAPI AI service. +- Do not mix tRPC and OpenAPI clients. +- Do not have the AI service call the application API. + +## Database & Validation + +- Use Drizzle ORM for database access. +- Use Zod for validation. +- Use `drizzle-zod` for deriving Zod schemas from Drizzle models. +- Do not duplicate schema definitions manually. +- Place non-database Zod schemas in the `packages/validators` package. + +## AI Service Integration + +- The AI service receives all required inputs (user context, metrics, documents). +- The AI service must not fetch user or metrics data on its own. +- Inputs should be explicit, minimal, and versionable. +- AI service shouldn't have a seperate auth flow it should instead use the AI_SERVICE_KEY env var to authenticate requests from the main app which will have that variable sent using a secure header. \ No newline at end of file diff --git a/.github/.copilot-instructions.md b/.github/.copilot-instructions.md index a8a2f30..f58ea44 100644 --- a/.github/.copilot-instructions.md +++ b/.github/.copilot-instructions.md @@ -3,6 +3,7 @@ - This is a PoC: prioritize developer speed and clarity over abstraction. - Treat all backends as APIs; do not couple services together. - The AI service is stateless and must not call application APIs directly. +- always use pnpm and pnpx for package management ## Monorepo Structure @@ -27,7 +28,7 @@ ## Frontend & UI -- All reusable UI components must live in the `packages/ui` package. +- All reusable UI components must live in the `packages/ui` package. use them - Use Tailwind CSS for styling. - Prefer shadcn/ui components where applicable. - Do not create ad-hoc UI components in app folders. @@ -41,7 +42,7 @@ ## APIs & Data Fetching - Use tRPC for type-safe application API calls (user data, metrics, app state). -- Use OpenAPI Generator to generate a typed client for the FastAPI AI service. +- Use @hey-api/openapi-ts to generate a typed client for the FastAPI AI service. - Do not mix tRPC and OpenAPI clients. - Do not have the AI service call the application API. diff --git a/.gitignore b/.gitignore index 9f749ad..d301947 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ dist/ # tanstack .tanstack +# generated SDK +packages/ai/src/ + # ========= python ========= # Python-generated files __pycache__/ diff --git a/apps/tanstack-start/package.json b/apps/tanstack-start/package.json index 4f8ceb5..e26f9de 100644 --- a/apps/tanstack-start/package.json +++ b/apps/tanstack-start/package.json @@ -29,7 +29,7 @@ "@trpc/client": "catalog:", "@trpc/server": "catalog:", "@trpc/tanstack-react-query": "catalog:", - "better-auth": "catalog:", + "@workos/authkit-tanstack-react-start": "^0.5.0", "nitro": "3.0.1-alpha.1", "react": "catalog:react19", "react-dom": "catalog:react19", diff --git a/apps/tanstack-start/src/auth/client.ts b/apps/tanstack-start/src/auth/client.ts deleted file mode 100644 index f1012dd..0000000 --- a/apps/tanstack-start/src/auth/client.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createAuthClient } from "better-auth/react"; - -export const authClient = createAuthClient(); diff --git a/apps/tanstack-start/src/auth/server.ts b/apps/tanstack-start/src/auth/server.ts deleted file mode 100644 index 5d59fe6..0000000 --- a/apps/tanstack-start/src/auth/server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { reactStartCookies } from "better-auth/react-start"; - -import { initAuth } from "@governance/auth"; - -import { env } from "~/env"; -import { getBaseUrl } from "~/lib/url"; - -export const auth = initAuth({ - baseUrl: getBaseUrl(), - productionUrl: `https://${env.VERCEL_PROJECT_PRODUCTION_URL ?? "turbo.t3.gg"}`, - secret: env.AUTH_SECRET, - discordClientId: env.AUTH_DISCORD_ID, - discordClientSecret: env.AUTH_DISCORD_SECRET, - - extraPlugins: [reactStartCookies()], -}); diff --git a/apps/tanstack-start/src/component/NotFound.tsx b/apps/tanstack-start/src/component/NotFound.tsx new file mode 100644 index 0000000..c4e06aa --- /dev/null +++ b/apps/tanstack-start/src/component/NotFound.tsx @@ -0,0 +1,26 @@ +import { Link, useLocation } from "@tanstack/react-router"; + +export function NotFound() { + const location = useLocation(); + console.log("NotFound triggered for path:", location.pathname); + + return ( +
+

404

+
+ Page Not Found +
+
+
+ Oops! The page you asked for doesn't exist. +
+ + Go Home + +
+
+ ); +} diff --git a/apps/tanstack-start/src/component/auth-showcase.tsx b/apps/tanstack-start/src/component/auth-showcase.tsx index 86a45c2..ff887d0 100644 --- a/apps/tanstack-start/src/component/auth-showcase.tsx +++ b/apps/tanstack-start/src/component/auth-showcase.tsx @@ -1,29 +1,30 @@ -import { useNavigate } from "@tanstack/react-router"; +import { useAuth } from "@workos/authkit-tanstack-react-start/client"; +import { getSignInUrl } from "@workos/authkit-tanstack-react-start"; +import { createServerFn } from "@tanstack/react-start"; import { Button } from "@governance/ui/button"; -import { authClient } from "~/auth/client"; +const getSignInUrlFn = createServerFn({ method: "GET" }).handler(async () => { + return await getSignInUrl(); +}); export function AuthShowcase() { - const { data: session } = authClient.useSession(); - const navigate = useNavigate(); + const { user, loading, signOut } = useAuth(); - if (!session) { + if (loading) { + return
Loading...
; + } + + if (!user) { return ( ); } @@ -31,16 +32,13 @@ export function AuthShowcase() { return (

- Logged in as {session.user.name} + + Logged in as {user.firstName} {user.lastName} +

+

{user.email}

-
diff --git a/apps/tanstack-start/src/env.ts b/apps/tanstack-start/src/env.ts index 66f6f20..32477d3 100644 --- a/apps/tanstack-start/src/env.ts +++ b/apps/tanstack-start/src/env.ts @@ -1,9 +1,8 @@ +import { authEnv } from "@governance/auth/env"; import { createEnv } from "@t3-oss/env-core"; import { vercel } from "@t3-oss/env-core/presets-zod"; import { z } from "zod/v4"; -import { authEnv } from "@governance/auth/env"; - export const env = createEnv({ clientPrefix: "VITE_", extends: [authEnv(), vercel()], @@ -22,10 +21,10 @@ export const env = createEnv({ /** * Specify your client-side environment variables schema here. - * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. + * For them to be exposed to the client, prefix them with `VITE_`. */ client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), + // VITE_CLIENTVAR: z.string(), }, /** * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. diff --git a/apps/tanstack-start/src/lib/trpc.ts b/apps/tanstack-start/src/lib/trpc.ts index 70df5fb..c066fde 100644 --- a/apps/tanstack-start/src/lib/trpc.ts +++ b/apps/tanstack-start/src/lib/trpc.ts @@ -8,10 +8,10 @@ import { } from "@trpc/client"; import { createTRPCContext } from "@trpc/tanstack-react-query"; import SuperJSON from "superjson"; +import { getAuth } from "@workos/authkit-tanstack-react-start"; import * as Api from "@governance/api"; -import { auth } from "~/auth/server"; import { env } from "~/env"; import { getBaseUrl } from "~/lib/url"; @@ -22,9 +22,10 @@ export const makeTRPCClient = createIsomorphicFn() unstable_localLink({ router: Api.appRouter, transformer: SuperJSON, - createContext: () => { + createContext: async () => { const headers = new Headers(getRequestHeaders()); headers.set("x-trpc-source", "tanstack-start-server"); + const auth = await getAuth(); return Api.createTRPCContext({ auth, headers }); }, }), diff --git a/apps/tanstack-start/src/routeTree.gen.ts b/apps/tanstack-start/src/routeTree.gen.ts index 14b20b8..3428522 100644 --- a/apps/tanstack-start/src/routeTree.gen.ts +++ b/apps/tanstack-start/src/routeTree.gen.ts @@ -11,7 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiTrpcSplatRouteImport } from './routes/api/trpc.$' -import { Route as ApiAuthSplatRouteImport } from './routes/api/auth.$' +import { Route as ApiAuthCallbackRouteImport } from './routes/api/auth/callback' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -23,39 +23,39 @@ const ApiTrpcSplatRoute = ApiTrpcSplatRouteImport.update({ path: '/api/trpc/$', getParentRoute: () => rootRouteImport, } as any) -const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({ - id: '/api/auth/$', - path: '/api/auth/$', +const ApiAuthCallbackRoute = ApiAuthCallbackRouteImport.update({ + id: '/api/auth/callback', + path: '/api/auth/callback', getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/api/auth/$': typeof ApiAuthSplatRoute + '/api/auth/callback': typeof ApiAuthCallbackRoute '/api/trpc/$': typeof ApiTrpcSplatRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/api/auth/$': typeof ApiAuthSplatRoute + '/api/auth/callback': typeof ApiAuthCallbackRoute '/api/trpc/$': typeof ApiTrpcSplatRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute - '/api/auth/$': typeof ApiAuthSplatRoute + '/api/auth/callback': typeof ApiAuthCallbackRoute '/api/trpc/$': typeof ApiTrpcSplatRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/api/auth/$' | '/api/trpc/$' + fullPaths: '/' | '/api/auth/callback' | '/api/trpc/$' fileRoutesByTo: FileRoutesByTo - to: '/' | '/api/auth/$' | '/api/trpc/$' - id: '__root__' | '/' | '/api/auth/$' | '/api/trpc/$' + to: '/' | '/api/auth/callback' | '/api/trpc/$' + id: '__root__' | '/' | '/api/auth/callback' | '/api/trpc/$' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - ApiAuthSplatRoute: typeof ApiAuthSplatRoute + ApiAuthCallbackRoute: typeof ApiAuthCallbackRoute ApiTrpcSplatRoute: typeof ApiTrpcSplatRoute } @@ -75,11 +75,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiTrpcSplatRouteImport parentRoute: typeof rootRouteImport } - '/api/auth/$': { - id: '/api/auth/$' - path: '/api/auth/$' - fullPath: '/api/auth/$' - preLoaderRoute: typeof ApiAuthSplatRouteImport + '/api/auth/callback': { + id: '/api/auth/callback' + path: '/api/auth/callback' + fullPath: '/api/auth/callback' + preLoaderRoute: typeof ApiAuthCallbackRouteImport parentRoute: typeof rootRouteImport } } @@ -87,7 +87,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - ApiAuthSplatRoute: ApiAuthSplatRoute, + ApiAuthCallbackRoute: ApiAuthCallbackRoute, ApiTrpcSplatRoute: ApiTrpcSplatRoute, } export const routeTree = rootRouteImport @@ -95,10 +95,11 @@ export const routeTree = rootRouteImport ._addFileTypes() import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' +import type { startInstance } from './start.ts' declare module '@tanstack/react-start' { interface Register { ssr: true router: Awaited> + config: Awaited> } } diff --git a/apps/tanstack-start/src/routes/__root.tsx b/apps/tanstack-start/src/routes/__root.tsx index 66bb2a5..91082f9 100644 --- a/apps/tanstack-start/src/routes/__root.tsx +++ b/apps/tanstack-start/src/routes/__root.tsx @@ -9,13 +9,20 @@ import { Scripts, } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import { AuthKitProvider } from "@workos/authkit-tanstack-react-start/client"; import type { AppRouter } from "@governance/api"; -import { ThemeProvider, ThemeToggle } from "@governance/ui/theme"; +import { + themeDetectorScript, + ThemeProvider, + ThemeToggle, +} from "@governance/ui/theme"; import { Toaster } from "@governance/ui/toast"; import appCss from "~/styles.css?url"; +import { NotFound } from "../component/NotFound"; + export const Route = createRootRouteWithContext<{ queryClient: QueryClient; trpc: TRPCOptionsProxy; @@ -24,13 +31,16 @@ export const Route = createRootRouteWithContext<{ links: [{ rel: "stylesheet", href: appCss }], }), component: RootComponent, + notFoundComponent: NotFound, }); function RootComponent() { return ( - - - + + + + + ); } @@ -40,6 +50,10 @@ function RootDocument({ children }: { children: React.ReactNode }) { +