Skip to content

Commit 3adcf11

Browse files
authored
TanStack Start SPA mode + catalog deps + Vite 8 (#89)
- Cloud app uses TanStack Start with SPA mode for unified dev/prod - File-based routes: pages, /v1/* API catch-all, /auth/* catch-all - Root route with auth gate + ExecutorProvider - Cloud-specific auth API group (CloudAuthApi) with typed atom - Upgrade Vite to v8, @vitejs/plugin-react to v6 across workspace - Catalog shared deps (vite, react, typescript, tanstack, tailwind) - Fix Tailwind @source paths for new package structure - Cloud shell with user footer, no version check
2 parents 4153df4 + 9fe5738 commit 3adcf11

68 files changed

Lines changed: 1340 additions & 1305 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ executor.har
4242
apps/desktop/resources/
4343

4444
# cloud local dev database
45-
.pglite
45+
.pglite

‎.gitmodules‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[submodule ".reference/effect-atom"]
2+
path = .reference/effect-atom
3+
url = https://github.com/tim-smart/effect-atom.git
4+
[submodule ".reference/tanstack-router"]
5+
path = .reference/tanstack-router
6+
url = https://github.com/TanStack/router.git
7+
[submodule ".reference/effect"]
8+
path = .reference/effect
9+
url = https://github.com/Effect-TS/effect.git

‎.reference/executor‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎.reference/tanstack-router‎

Submodule tanstack-router added at 5a81726

‎apps/cli/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@
3030
},
3131
"devDependencies": {
3232
"bun-types": "catalog:",
33-
"typescript": "^5.9.3"
33+
"typescript": "catalog:"
3434
}
3535
}

‎apps/cloud/index.html‎

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎apps/cloud/package.json‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,30 @@
1515
"@executor/react": "workspace:*",
1616
"@executor/sdk": "workspace:*",
1717
"@executor/storage-postgres": "workspace:*",
18-
"@tanstack/react-router": "^1.168.10",
18+
"@tanstack/react-router": "catalog:",
19+
"@tanstack/react-start": "catalog:",
1920
"@workos-inc/node": "^7.0.0",
2021
"drizzle-orm": "catalog:",
2122
"effect": "catalog:",
2223
"pg": "^8.16.0",
23-
"react": "^19.1.0",
24-
"react-dom": "^19.1.0"
24+
"react": "catalog:",
25+
"react-dom": "catalog:"
2526
},
2627
"devDependencies": {
2728
"@electric-sql/pglite": "^0.4.3",
29+
"@tailwindcss/vite": "catalog:",
2830
"@types/pg": "^8.15.4",
29-
"@tailwindcss/vite": "^4.1.10",
30-
"@types/react": "^19.1.0",
31-
"@types/react-dom": "^19.1.0",
32-
"@vitejs/plugin-react": "^4.5.2",
31+
"@types/react": "catalog:",
32+
"@types/react-dom": "catalog:",
33+
"@vitejs/plugin-react": "catalog:",
3334
"portless": "^0.3.0",
34-
"typescript": "^5.9.3",
35-
"vite": "^6.3.5"
35+
"typescript": "catalog:",
36+
"vite": "catalog:"
3637
},
3738
"scripts": {
38-
"dev": "op run --env-file=.env -- portless executor-cloud bun --hot ./src/main.ts",
39-
"dev:web": "vite",
40-
"start": "bun ./src/main.ts",
41-
"build:web": "vite build",
39+
"dev": "portless executor-cloud op run --env-file=.env -- vite dev",
40+
"build": "vite build",
41+
"start": "bun ./server.ts",
4242
"typecheck": "tsc --noEmit"
4343
}
4444
}

‎apps/cloud/src/api.ts‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ import { Effect, Layer } from "effect";
1212

1313
import { addGroup, CoreHandlers, ExecutorService, ExecutionEngineService } from "@executor/api";
1414
import { createExecutionEngine } from "@executor/execution";
15-
import { makeUserStore } from "@executor/storage-postgres";
1615
import { OpenApiGroup, OpenApiExtensionService, OpenApiHandlers } from "@executor/plugin-openapi/api";
1716
import { McpGroup, McpExtensionService, McpHandlers } from "@executor/plugin-mcp/api";
1817
import { GoogleDiscoveryGroup, GoogleDiscoveryExtensionService, GoogleDiscoveryHandlers } from "@executor/plugin-google-discovery/api";
1918
import { GraphqlGroup, GraphqlExtensionService, GraphqlHandlers } from "@executor/plugin-graphql/api";
2019

2120
import { createTeamExecutor } from "./services/executor";
22-
import { authenticateRequest } from "./auth/workos";
2321
import { CloudAuthApi } from "./auth/api";
2422
import { CloudAuthHandlers } from "./auth/handlers";
2523
import { AuthContext, UserStoreService } from "./auth/context";
24+
import { WorkOSAuth } from "./auth/workos";
2625
import type { DrizzleDb } from "./services/db";
2726

2827
// ---------------------------------------------------------------------------
@@ -65,10 +64,15 @@ const parseCookie = (cookieHeader: string | null, name: string): string | null =
6564
// ---------------------------------------------------------------------------
6665

6766
export const createCloudApiHandler = (db: DrizzleDb, encryptionKey: string) => {
68-
const userStore = makeUserStore(db);
69-
7067
return async (request: Request): Promise<Response> => {
71-
const auth = await authenticateRequest(request);
68+
// Authenticate via WorkOS sealed session
69+
const auth = await Effect.runPromise(
70+
Effect.gen(function* () {
71+
const workos = yield* WorkOSAuth;
72+
return yield* workos.authenticateRequest(request);
73+
}).pipe(Effect.provide(WorkOSAuth.Default)),
74+
);
75+
7276
if (!auth) {
7377
return Response.json({ error: "Unauthorized" }, { status: 401 });
7478
}
@@ -78,7 +82,14 @@ export const createCloudApiHandler = (db: DrizzleDb, encryptionKey: string) => {
7882
return Response.json({ error: "No team selected" }, { status: 401 });
7983
}
8084

81-
const team = await userStore.getTeam(teamId);
85+
// Resolve team name
86+
const userStore = UserStoreService.layer(db);
87+
const team = await Effect.runPromise(
88+
Effect.gen(function* () {
89+
const users = yield* UserStoreService;
90+
return yield* users.use((s) => s.getTeam(teamId));
91+
}).pipe(Effect.provide(userStore)),
92+
);
8293
const teamName = team?.name ?? "Unknown Team";
8394

8495
const executor = await Effect.runPromise(
@@ -108,7 +119,8 @@ export const createCloudApiHandler = (db: DrizzleDb, encryptionKey: string) => {
108119
name: `${auth.firstName ?? ""} ${auth.lastName ?? ""}`.trim() || null,
109120
avatarUrl: auth.avatarUrl,
110121
})),
111-
Layer.provideMerge(Layer.succeed(UserStoreService, userStore)),
122+
Layer.provideMerge(UserStoreService.layer(db)),
123+
Layer.provideMerge(WorkOSAuth.Default),
112124
Layer.provideMerge(HttpServer.layerContext),
113125
),
114126
{ middleware: HttpMiddleware.logger },

‎apps/cloud/src/auth/api.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
22
import { Schema } from "effect";
3+
import { UserStoreError, WorkOSError } from "./context";
34

45
const AuthUser = Schema.Struct({
56
id: Schema.String,
@@ -18,8 +19,25 @@ const AuthMeResponse = Schema.Struct({
1819
team: Schema.NullOr(AuthTeam),
1920
});
2021

22+
const AuthCallbackSearch = Schema.Struct({
23+
code: Schema.String,
24+
});
25+
2126
export class CloudAuthApi extends HttpApiGroup.make("cloudAuth")
2227
.add(
2328
HttpApiEndpoint.get("me")`/auth/me`
24-
.addSuccess(AuthMeResponse),
29+
.addSuccess(AuthMeResponse)
30+
.addError(UserStoreError),
31+
)
32+
.add(
33+
HttpApiEndpoint.get("login")`/auth/login`,
34+
)
35+
.add(
36+
HttpApiEndpoint.get("callback")`/auth/callback`
37+
.setUrlParams(AuthCallbackSearch)
38+
.addError(UserStoreError)
39+
.addError(WorkOSError),
40+
)
41+
.add(
42+
HttpApiEndpoint.post("logout")`/auth/logout`,
2543
) {}

‎apps/cloud/src/auth/context.ts‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1-
import { Context } from "effect";
2-
import type { makeUserStore } from "@executor/storage-postgres";
1+
import { Context, Effect, Layer, Schema } from "effect";
2+
import { makeUserStore } from "@executor/storage-postgres";
3+
import type { DrizzleDb } from "../services/db";
4+
5+
// ---------------------------------------------------------------------------
6+
// Errors
7+
// ---------------------------------------------------------------------------
8+
9+
export class UserStoreError extends Schema.TaggedError<UserStoreError>()(
10+
"UserStoreError",
11+
{ cause: Schema.Unknown },
12+
) {}
13+
14+
export class WorkOSError extends Schema.TaggedError<WorkOSError>()(
15+
"WorkOSError",
16+
{ cause: Schema.Unknown },
17+
) {}
18+
19+
// ---------------------------------------------------------------------------
20+
// AuthContext — resolved per-request from sealed session
21+
// ---------------------------------------------------------------------------
322

423
export class AuthContext extends Context.Tag("@executor/cloud/AuthContext")<
524
AuthContext,
@@ -12,7 +31,30 @@ export class AuthContext extends Context.Tag("@executor/cloud/AuthContext")<
1231
}
1332
>() {}
1433

34+
// ---------------------------------------------------------------------------
35+
// UserStoreService — wraps the Drizzle-backed user store with Effect
36+
// ---------------------------------------------------------------------------
37+
38+
type RawStore = ReturnType<typeof makeUserStore>;
39+
40+
const makeService = (db: DrizzleDb) => {
41+
const store = makeUserStore(db);
42+
43+
const use = <A>(fn: (s: RawStore) => Promise<A>) =>
44+
Effect.tryPromise({
45+
try: () => fn(store),
46+
catch: (cause) => new UserStoreError({ cause }),
47+
}).pipe(Effect.withSpan("user_store"));
48+
49+
return { use };
50+
};
51+
52+
type UserStoreServiceType = ReturnType<typeof makeService>;
53+
1554
export class UserStoreService extends Context.Tag("@executor/cloud/UserStoreService")<
1655
UserStoreService,
17-
ReturnType<typeof makeUserStore>
18-
>() {}
56+
UserStoreServiceType
57+
>() {
58+
static layer = (db: DrizzleDb) =>
59+
Layer.succeed(this, makeService(db));
60+
}

0 commit comments

Comments
 (0)