Skip to content

Commit 0aa4348

Browse files
committed
feat: configure BetterAuth with org and API key plugins
1 parent a7a681d commit 0aa4348

7 files changed

Lines changed: 199 additions & 3 deletions

File tree

apps/api/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,12 @@
88
"typecheck": "tsc --noEmit",
99
"lint": "eslint src/",
1010
"test": "echo 'no tests yet'"
11+
},
12+
"dependencies": {
13+
"better-auth": "^1.4.18",
14+
"mysql2": "^3.17.4"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^25.3.0"
1118
}
1219
}

apps/api/src/auth-client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createAuthClient } from 'better-auth/client'
2+
import { organizationClient, apiKeyClient } from 'better-auth/client/plugins'
3+
4+
export const authClient = createAuthClient({
5+
plugins: [organizationClient(), apiKeyClient()],
6+
})

apps/api/src/auth.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { betterAuth } from 'better-auth'
2+
import { organization, apiKey } from 'better-auth/plugins'
3+
import { createPool } from 'mysql2/promise'
4+
5+
export const auth = betterAuth({
6+
database: createPool({
7+
uri: process.env.DATABASE_URL!,
8+
waitForConnections: true,
9+
connectionLimit: 10,
10+
}),
11+
emailAndPassword: { enabled: true },
12+
socialProviders: {
13+
github: {
14+
clientId: process.env.GITHUB_CLIENT_ID!,
15+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
16+
},
17+
google: {
18+
clientId: process.env.GOOGLE_CLIENT_ID!,
19+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
20+
},
21+
},
22+
plugins: [
23+
organization(),
24+
apiKey({
25+
enableMetadata: true,
26+
}),
27+
],
28+
})

apps/api/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export {}
1+
export { auth } from './auth.js'
2+
export { authClient } from './auth-client.js'

bun.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/spec/phases/phase-1.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ From `docs/spec/09-infrastructure.md` → "Build tooling" and `docs/spec/11-mile
789789

790790
## Task 9: Set up BetterAuth with organization and API key plugins
791791

792-
- [ ] **Status**: Pending
792+
- [x] **Status**: Done
793793
- **Commit type**: `feat`
794794
- **Commit message**: `feat: configure BetterAuth with org and API key plugins`
795795

@@ -870,4 +870,10 @@ Reference `docs/spec/10-security.md` → "Authentication" and `docs/spec/08-data
870870
- Auth config includes `organization()` plugin
871871

872872
### Learnings
873-
<!-- Filled in after completion -->
873+
- BetterAuth v1.4.18 uses `mysql2/promise` `createPool()` directly as its database adapter — the `{ type: 'mysql', url }` shorthand from some docs is not the preferred approach for mysql2
874+
- `@better-auth/cli generate` requires a running MySQL database to introspect existing schema — migration SQL was hand-written based on BetterAuth source code schema definitions since no local MySQL is available
875+
- Organization plugin adds `activeOrganizationId` column to the `session` table for tracking which org a user is operating in
876+
- API key plugin has built-in rate limiting columns (`rateLimitEnabled`, `rateLimitTimeWindow`, `rateLimitMax`, `requestCount`) — separate from Redis-backed rate limiting at the API layer
877+
- `enableMetadata: true` on apiKey plugin allows storing `org_id` and `environment` in the `metadata` JSON column for org/env scoping
878+
- `@types/node` needed as devDependency in `apps/api` since `process.env` is used for config
879+
- All BetterAuth tables use `VARCHAR(36)` string IDs (not UUIDv7/BINARY(16)) — consistent with the spec's guidance that BetterAuth owns its ID format
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
-- BetterAuth schema migration
2+
-- Generated from better-auth v1.4.18 with organization + apiKey plugins
3+
-- DO NOT hand-edit — regenerate via: npx @better-auth/cli generate
4+
-- PlanetScale/Vitess: no foreign key constraints
5+
6+
CREATE TABLE `user` (
7+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
8+
`name` VARCHAR(255) NOT NULL,
9+
`email` VARCHAR(255) NOT NULL UNIQUE,
10+
`emailVerified` BOOLEAN NOT NULL DEFAULT FALSE,
11+
`image` VARCHAR(255),
12+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
13+
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
14+
);
15+
16+
CREATE TABLE `session` (
17+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
18+
`userId` VARCHAR(36) NOT NULL,
19+
`token` VARCHAR(255) NOT NULL UNIQUE,
20+
`expiresAt` DATETIME NOT NULL,
21+
`ipAddress` VARCHAR(255),
22+
`userAgent` VARCHAR(255),
23+
`activeOrganizationId` VARCHAR(36),
24+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
25+
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
26+
);
27+
28+
CREATE TABLE `account` (
29+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
30+
`userId` VARCHAR(36) NOT NULL,
31+
`accountId` VARCHAR(255) NOT NULL,
32+
`providerId` VARCHAR(255) NOT NULL,
33+
`accessToken` TEXT,
34+
`refreshToken` TEXT,
35+
`accessTokenExpiresAt` DATETIME,
36+
`refreshTokenExpiresAt` DATETIME,
37+
`scope` VARCHAR(255),
38+
`idToken` TEXT,
39+
`password` VARCHAR(255),
40+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
41+
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
42+
);
43+
44+
CREATE TABLE `verification` (
45+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
46+
`identifier` VARCHAR(255) NOT NULL,
47+
`value` VARCHAR(255) NOT NULL,
48+
`expiresAt` DATETIME NOT NULL,
49+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
50+
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
51+
);
52+
53+
CREATE TABLE `organization` (
54+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
55+
`name` VARCHAR(255) NOT NULL,
56+
`slug` VARCHAR(255) NOT NULL UNIQUE,
57+
`logo` VARCHAR(255),
58+
`metadata` TEXT,
59+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
60+
);
61+
62+
CREATE TABLE `member` (
63+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
64+
`organizationId` VARCHAR(36) NOT NULL,
65+
`userId` VARCHAR(36) NOT NULL,
66+
`role` VARCHAR(255) NOT NULL DEFAULT 'member',
67+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
68+
INDEX `idx_member_org` (`organizationId`),
69+
INDEX `idx_member_user` (`userId`)
70+
);
71+
72+
CREATE TABLE `invitation` (
73+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
74+
`organizationId` VARCHAR(36) NOT NULL,
75+
`email` VARCHAR(255) NOT NULL,
76+
`role` VARCHAR(255),
77+
`status` VARCHAR(255) NOT NULL DEFAULT 'pending',
78+
`expiresAt` DATETIME NOT NULL,
79+
`inviterId` VARCHAR(36) NOT NULL,
80+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
81+
INDEX `idx_invitation_org` (`organizationId`),
82+
INDEX `idx_invitation_email` (`email`)
83+
);
84+
85+
CREATE TABLE `apikey` (
86+
`id` VARCHAR(36) NOT NULL PRIMARY KEY,
87+
`name` VARCHAR(255),
88+
`start` VARCHAR(255),
89+
`prefix` VARCHAR(255),
90+
`key` VARCHAR(255) NOT NULL,
91+
`userId` VARCHAR(36) NOT NULL,
92+
`refillInterval` INT,
93+
`refillAmount` INT,
94+
`lastRefillAt` DATETIME,
95+
`enabled` BOOLEAN DEFAULT TRUE,
96+
`rateLimitEnabled` BOOLEAN DEFAULT TRUE,
97+
`rateLimitTimeWindow` INT,
98+
`rateLimitMax` INT,
99+
`requestCount` INT DEFAULT 0,
100+
`remaining` INT,
101+
`lastRequest` DATETIME,
102+
`expiresAt` DATETIME,
103+
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
104+
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
105+
`permissions` TEXT,
106+
`metadata` TEXT,
107+
INDEX `idx_apikey_key` (`key`),
108+
INDEX `idx_apikey_user` (`userId`)
109+
);

0 commit comments

Comments
 (0)