Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
30a282e
feat(auth): enable Better Auth organizations and generated schema
mateoroldos Sep 18, 2026
225e5c9
feat(core): authorize organization-owned todos by capability
mateoroldos Sep 18, 2026
f681784
chore(test): simplify validation to colocated Vitest and PGlite suites
mateoroldos Sep 18, 2026
b1abac6
feat(web): replace workspaces with organizations and shared todos
mateoroldos Sep 18, 2026
1cde83e
fix(web): preserve failure causes and request cancellation at framewo…
mateoroldos Sep 18, 2026
322640a
refactor(web): clarify runtime and operation outcomes in request runner
mateoroldos Sep 22, 2026
d195a3f
refactor(telemetry): label parent spans by request kind
mateoroldos Sep 22, 2026
fc969a3
refactor(web): compose request runtime directly from SvelteKit event
mateoroldos Sep 22, 2026
e20dbf4
refactor(telemetry): name operations by entrypoint boundary
mateoroldos Sep 22, 2026
11d8bf6
refactor(telemetry): derive remote parent names from existing operations
mateoroldos Sep 22, 2026
7744927
fix(web): infer server error hook input from SvelteKit contract
mateoroldos Sep 22, 2026
2125842
feat(web): add documented pending boundaries for organization and tod…
mateoroldos Sep 22, 2026
6593463
docs(web): record verified remote-query preloading constraints
mateoroldos Sep 22, 2026
a015338
feat(web): preload component queries with shared organization layout
mateoroldos Sep 22, 2026
ae37fae
docs(skill): make frontend loading guidance concise and actionable
mateoroldos Sep 22, 2026
d8841f2
fix(web): disable forked preloading to avoid cold-hover crashes
mateoroldos Sep 22, 2026
0b86234
fix(web): use direct query overrides for optimistic todos
mateoroldos Sep 22, 2026
9e1a7b2
feat(todos): add optional multiline descriptions
mateoroldos Sep 22, 2026
5cbd308
refactor(todos): clarify optimistic row ownership and recovery
mateoroldos Sep 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,6 @@ jobs:

- run: bun run check
- run: bun run build
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install Chromium
run: bun run --cwd apps/web playwright install --with-deps chromium
- name: Run browser acceptance test
id: e2e
run: bun run test:e2e
- name: Upload Playwright failure report
uses: actions/upload-artifact@v4
if: ${{ failure() && steps.e2e.outcome == 'failure' }}
with:
name: playwright-report
path: apps/web/playwright-report/
retention-days: 7

deploy_preview:
name: Deploy labeled PR preview
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@
.turbo
node_modules
*.tsbuildinfo
playwright-report/
test-results/
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ui → nothing
- Remote functions own application calls from the browser. A `query` is the cache; a `form` is the mutation. Do not add a client-side state library.
- Remote handlers invoke core capabilities directly.
- Better Auth is the deliberate exception: its Svelte client calls the same-origin `/api/auth/*` routes directly so the provider owns its browser protocol and cookies.
- Better Auth owns organizations and memberships. Core owns application permissions; require the capability through `OrganizationAccess` and scope persistence by the explicit organization ID.
- Effect Schema validates remote form input through Standard Schema. Remote handlers map expected failures to SvelteKit `error` or `invalid` results.
- Svelte runes are limited to component-local presentation state.
- Server layouts resolve principals required by their pages; remote handlers and endpoints own authorization for their operations.
Expand All @@ -45,6 +46,6 @@ ui → nothing

## Tests

Test through public interfaces using substitute Layers, PGlite, and real HTTP or SvelteKit boundaries. Do not mock modules or use arbitrary sleeps.
Test through public interfaces using substitute Layers, PGlite, and provider HTTP handlers. Do not mock modules or use arbitrary sleeps.

A change is complete only after type checks, tests, formatting, architecture checks, and dead-code checks pass.
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ alchemy.run.ts application Stack summary

SvelteKit remote functions call application services directly. The Web Worker hosts Better Auth behind same-origin routes and projects authenticated principals into application operations. Independent API and CLI clients are added only when their concrete contracts and credentials are known.

The example flow is a shared todo list: sign up, create an organization, and add or
complete todos with its members. Better Auth owns organizations and memberships;
`TodoDirectory` owns application permissions and `TodoStore` owns organization-scoped persistence.

## Development

Bootstrap a checkout and authenticate the local Alchemy profile:
Expand Down Expand Up @@ -79,25 +83,22 @@ single-line logfmt; production uses JSON. Normal `bun run dev` needs no collecto

See [operation logging](skills/effect-forge/references/observability.md) for examples.

## Organization URLs

Organization URLs use handles, for example `/organizations/analytical-engine/todos`.
Authorization and persistence use the stable organization ID. Handles can be changed
through Better Auth; changing one changes its URL without redirecting old links.

## Validation

```sh
bun run test # tests only
bun run test # colocated tests
bun run check # formatting, lint, types, and tests
bun run build # production builds
```

CI runs `check`, `build`, and the browser acceptance test.

### Browser acceptance test

With Docker running:

```sh
bun run test:e2e
```

This runs the authentication journey against the built app and a disposable PostgreSQL database.
CI runs `check` and `build`. Colocated tests cover domain and application behavior,
provider HTTP handling, and SQL through PGlite.

### Database changes

Expand Down
2 changes: 1 addition & 1 deletion adapters/database-postgres/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
out: "./drizzle",
schema: ["./src/auth/schema.ts", "./src/workspace/schema.ts"],
schema: ["./src/auth/schema.ts", "./src/todo/schema.ts"],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
CREATE TABLE "invitation" (
"id" text PRIMARY KEY,
"organization_id" text NOT NULL,
"email" text NOT NULL,
"role" text,
"status" text DEFAULT 'pending' NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"inviter_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "member" (
"id" text PRIMARY KEY,
"organization_id" text NOT NULL,
"user_id" text NOT NULL,
"role" text DEFAULT 'member' NOT NULL,
"created_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organization" (
"id" text PRIMARY KEY,
"name" text NOT NULL,
"slug" text NOT NULL UNIQUE,
"logo" text,
"created_at" timestamp NOT NULL,
"metadata" text
);
--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "active_organization_id" text;--> statement-breakpoint
CREATE INDEX "invitation_organizationId_idx" ON "invitation" ("organization_id");--> statement-breakpoint
CREATE INDEX "invitation_email_idx" ON "invitation" ("email");--> statement-breakpoint
CREATE INDEX "member_organizationId_idx" ON "member" ("organization_id");--> statement-breakpoint
CREATE INDEX "member_userId_idx" ON "member" ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "organization_slug_uidx" ON "organization" ("slug");--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fkey" FOREIGN KEY ("inviter_id") REFERENCES "user"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE;
Loading
Loading