@withmata/blueprints 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude/commands/audit.md +179 -0
  2. package/.claude/commands/discover.md +92 -0
  3. package/.claude/commands/new-blueprint.md +265 -0
  4. package/.claude/commands/new-project.md +230 -0
  5. package/.claude/commands/scaffold-auth.md +310 -0
  6. package/.claude/commands/scaffold-db.md +270 -0
  7. package/.claude/commands/scaffold-foundation.md +158 -0
  8. package/.cursor/commands/audit.md +179 -0
  9. package/.cursor/commands/discover.md +92 -0
  10. package/.cursor/commands/new-blueprint.md +265 -0
  11. package/.cursor/commands/new-project.md +230 -0
  12. package/.cursor/commands/scaffold-auth.md +310 -0
  13. package/.cursor/commands/scaffold-db.md +270 -0
  14. package/.cursor/commands/scaffold-foundation.md +158 -0
  15. package/.opencode/commands/audit.md +183 -0
  16. package/.opencode/commands/discover.md +96 -0
  17. package/.opencode/commands/new-blueprint.md +269 -0
  18. package/.opencode/commands/new-project.md +234 -0
  19. package/.opencode/commands/scaffold-auth.md +314 -0
  20. package/.opencode/commands/scaffold-db.md +274 -0
  21. package/.opencode/commands/scaffold-foundation.md +162 -0
  22. package/ENGINEERING.md +47 -0
  23. package/blueprints/discovery/product-discovery/BLUEPRINT.md +361 -0
  24. package/blueprints/discovery/product-discovery/templates/archetype-template.md +143 -0
  25. package/blueprints/discovery/product-discovery/templates/product-brief.md +65 -0
  26. package/blueprints/discovery/product-discovery/templates/product-thesis.md +64 -0
  27. package/blueprints/discovery/product-discovery/templates/research-summary.md +43 -0
  28. package/blueprints/features/auth-better-auth/BLUEPRINT.md +794 -0
  29. package/blueprints/features/auth-better-auth/files/client/auth-client.ts +31 -0
  30. package/blueprints/features/auth-better-auth/files/client/context/organization.ts +236 -0
  31. package/blueprints/features/auth-better-auth/files/client/hooks/use-create-organization.ts +45 -0
  32. package/blueprints/features/auth-better-auth/files/client/hooks/use-has-permission.ts +26 -0
  33. package/blueprints/features/auth-better-auth/files/client/hooks/use-organization.ts +64 -0
  34. package/blueprints/features/auth-better-auth/files/client/schema/auth.ts +21 -0
  35. package/blueprints/features/auth-better-auth/files/client/schema/organization.ts +51 -0
  36. package/blueprints/features/auth-better-auth/files/client/types/organization.ts +20 -0
  37. package/blueprints/features/auth-better-auth/files/db/auth-schema.ts +184 -0
  38. package/blueprints/features/auth-better-auth/files/db/drizzle.config.ts +21 -0
  39. package/blueprints/features/auth-better-auth/files/middleware/route-protection.ts +84 -0
  40. package/blueprints/features/auth-better-auth/files/server/auth-db.ts +18 -0
  41. package/blueprints/features/auth-better-auth/files/server/auth.ts +159 -0
  42. package/blueprints/features/auth-better-auth/files/server/env.ts +44 -0
  43. package/blueprints/features/auth-better-auth/files/server/middleware.ts +144 -0
  44. package/blueprints/features/db-drizzle-postgres/BLUEPRINT.md +596 -0
  45. package/blueprints/features/db-drizzle-postgres/files/db/drizzle.config.ts +18 -0
  46. package/blueprints/features/db-drizzle-postgres/files/db/env.example +3 -0
  47. package/blueprints/features/db-drizzle-postgres/files/db/package.json +33 -0
  48. package/blueprints/features/db-drizzle-postgres/files/db/src/client.ts +34 -0
  49. package/blueprints/features/db-drizzle-postgres/files/db/src/example-entity.ts +73 -0
  50. package/blueprints/features/db-drizzle-postgres/files/db/src/index.ts +8 -0
  51. package/blueprints/features/db-drizzle-postgres/files/db/src/scripts/seed.ts +50 -0
  52. package/blueprints/features/db-drizzle-postgres/files/db/tsconfig.json +13 -0
  53. package/blueprints/features/tailwind-v4/BLUEPRINT.md +29 -0
  54. package/blueprints/features/ui-shared-components/BLUEPRINT.md +35 -0
  55. package/blueprints/foundation/monorepo-turbo/BLUEPRINT.md +378 -0
  56. package/blueprints/foundation/monorepo-turbo/files/apps/web/app/globals.css +2 -0
  57. package/blueprints/foundation/monorepo-turbo/files/apps/web/app/layout.tsx +23 -0
  58. package/blueprints/foundation/monorepo-turbo/files/apps/web/app/page.tsx +7 -0
  59. package/blueprints/foundation/monorepo-turbo/files/apps/web/env.ts +13 -0
  60. package/blueprints/foundation/monorepo-turbo/files/apps/web/next.config.ts +12 -0
  61. package/blueprints/foundation/monorepo-turbo/files/apps/web/package.json +28 -0
  62. package/blueprints/foundation/monorepo-turbo/files/apps/web/postcss.config.mjs +5 -0
  63. package/blueprints/foundation/monorepo-turbo/files/apps/web/tsconfig.json +18 -0
  64. package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/package.json +14 -0
  65. package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/postcss.config.mjs +5 -0
  66. package/blueprints/foundation/monorepo-turbo/files/config/tailwind-config/shared-styles.css +88 -0
  67. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/base.json +19 -0
  68. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/nextjs.json +12 -0
  69. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/package.json +5 -0
  70. package/blueprints/foundation/monorepo-turbo/files/config/typescript-config/react-library.json +7 -0
  71. package/blueprints/foundation/monorepo-turbo/files/root/biome.json +46 -0
  72. package/blueprints/foundation/monorepo-turbo/files/root/gitignore +35 -0
  73. package/blueprints/foundation/monorepo-turbo/files/root/package.json +25 -0
  74. package/blueprints/foundation/monorepo-turbo/files/root/pnpm-workspace.yaml +5 -0
  75. package/blueprints/foundation/monorepo-turbo/files/root/turbo.json +30 -0
  76. package/dist/index.js +453 -0
  77. package/package.json +53 -0
@@ -0,0 +1,31 @@
1
+ // =============================================================================
2
+ // Better Auth Client Setup
3
+ // =============================================================================
4
+ // Creates the auth client for React/Next.js applications. This is the single
5
+ // entry point for all client-side auth operations (sign in, sign up, session
6
+ // management, organization operations).
7
+ //
8
+ // The baseURL logic handles two deployment modes:
9
+ // - Standalone server (e.g. Hono): In local dev, point directly to the API
10
+ // server. In production, proxy through the frontend (e.g. Vercel rewrites)
11
+ // so cookies stay on the same domain.
12
+ // - Next.js API routes: Always use relative URL.
13
+ // =============================================================================
14
+
15
+ import { organizationClient } from "better-auth/client/plugins";
16
+ import { createAuthClient } from "better-auth/react";
17
+ import { env } from "../env"; // CONFIGURE: adjust path to your env file
18
+
19
+ // CONFIGURE: Adjust baseURL logic for your deployment
20
+ const isLocalDev = env.NEXT_PUBLIC_API_URL.includes("localhost");
21
+ const BASE_URL = isLocalDev
22
+ ? `${env.NEXT_PUBLIC_API_URL}/api/auth`
23
+ : `${env.NEXT_PUBLIC_FRONTEND_URL}/api/auth`;
24
+
25
+ const authClient = createAuthClient({
26
+ baseURL: BASE_URL,
27
+ // CONFIGURE: Remove organizationClient() if not using the organization plugin
28
+ plugins: [organizationClient()],
29
+ });
30
+
31
+ export { authClient };
@@ -0,0 +1,236 @@
1
+ // =============================================================================
2
+ // Organization Context
3
+ // =============================================================================
4
+ // Provides organization state and actions to the React component tree.
5
+ // Split into TWO separate contexts for render performance:
6
+ // - OrganizationDataContext: reactive data (currentOrg, orgs list, role, etc.)
7
+ // - OrganizationActionsContext: stable action functions (switch, create, invite, etc.)
8
+ //
9
+ // Components that only call actions won't re-render when data changes.
10
+ //
11
+ // CONFIGURE: Remove this entire file if not using the organization plugin.
12
+ // =============================================================================
13
+
14
+ import { createContext, useCallback, useMemo, useRef, useState } from "react";
15
+ import { authClient } from "../auth-client";
16
+ import type {
17
+ Organization,
18
+ OrganizationWithRole,
19
+ } from "../types/organization";
20
+
21
+ // ============================================================================
22
+ // Types
23
+ // ============================================================================
24
+
25
+ interface OrganizationData {
26
+ currentOrganization: Organization | null;
27
+ organizations: OrganizationWithRole[];
28
+ currentRole: string | null;
29
+ isLoading: boolean;
30
+ needsOnboarding: boolean;
31
+ }
32
+
33
+ interface OrganizationActions {
34
+ switchOrganization: (organizationId: string) => Promise<void>;
35
+ createOrganization: (name: string, slug?: string) => Promise<Organization>;
36
+ inviteMember: (email: string, role: "admin" | "member") => Promise<void>;
37
+ removeMember: (memberId: string) => Promise<void>;
38
+ updateMemberRole: (
39
+ memberId: string,
40
+ role: "admin" | "member",
41
+ ) => Promise<void>;
42
+ leaveOrganization: () => Promise<void>;
43
+ refetch: () => void;
44
+ }
45
+
46
+ interface OrganizationContextValue
47
+ extends OrganizationData,
48
+ OrganizationActions {}
49
+
50
+ // ============================================================================
51
+ // Contexts
52
+ // ============================================================================
53
+
54
+ const OrganizationDataContext = createContext<OrganizationData | null>(null);
55
+ const OrganizationActionsContext = createContext<OrganizationActions | null>(
56
+ null,
57
+ );
58
+
59
+ // ============================================================================
60
+ // Hook — used by the Provider component
61
+ // ============================================================================
62
+
63
+ const useOrganizationContext = () => {
64
+ const { data: activeOrg, isPending: isActiveOrgLoading } =
65
+ authClient.useActiveOrganization();
66
+ const {
67
+ data: orgsData,
68
+ isPending: isOrgsLoading,
69
+ refetch,
70
+ } = authClient.useListOrganizations();
71
+ const { data: activeMember } = authClient.useActiveMember();
72
+
73
+ const [isSwitching, setIsSwitching] = useState(false);
74
+ const isLoading = isActiveOrgLoading || isOrgsLoading || isSwitching;
75
+
76
+ const currentOrgIdRef = useRef<string | null>(null);
77
+
78
+ const currentOrganization = useMemo(() => {
79
+ if (!activeOrg) {
80
+ currentOrgIdRef.current = null;
81
+ return null;
82
+ }
83
+ currentOrgIdRef.current = activeOrg.id;
84
+ return {
85
+ id: activeOrg.id,
86
+ name: activeOrg.name,
87
+ slug: activeOrg.slug,
88
+ logo: activeOrg.logo,
89
+ createdAt: activeOrg.createdAt,
90
+ metadata: activeOrg.metadata,
91
+ } as Organization;
92
+ }, [activeOrg]);
93
+
94
+ const organizations = useMemo(() => {
95
+ if (!orgsData) return [];
96
+ return orgsData.map((org) => ({
97
+ id: org.id,
98
+ name: org.name,
99
+ slug: org.slug,
100
+ logo: org.logo ?? null,
101
+ createdAt: org.createdAt,
102
+ metadata: org.metadata ?? null,
103
+ role: "member",
104
+ })) as OrganizationWithRole[];
105
+ }, [orgsData]);
106
+
107
+ const needsOnboarding = !isLoading && organizations.length === 0;
108
+
109
+ const currentRole = useMemo(() => {
110
+ return activeMember?.role ?? null;
111
+ }, [activeMember]);
112
+
113
+ const switchOrganization = useCallback(async (organizationId: string) => {
114
+ setIsSwitching(true);
115
+ try {
116
+ await authClient.organization.setActive({ organizationId });
117
+ } finally {
118
+ setIsSwitching(false);
119
+ }
120
+ }, []);
121
+
122
+ const createOrganization = useCallback(
123
+ async (name: string, slug?: string) => {
124
+ const result = await authClient.organization.create({
125
+ name,
126
+ slug: slug || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
127
+ });
128
+
129
+ if (result.error) {
130
+ throw new Error(result.error.message);
131
+ }
132
+
133
+ refetch();
134
+ return result.data as Organization;
135
+ },
136
+ [refetch],
137
+ );
138
+
139
+ const inviteMember = useCallback(
140
+ async (email: string, role: "admin" | "member") => {
141
+ const currentOrgId = currentOrgIdRef.current;
142
+ if (!currentOrgId) throw new Error("No active organization");
143
+
144
+ const result = await authClient.organization.inviteMember({
145
+ email,
146
+ role,
147
+ organizationId: currentOrgId,
148
+ });
149
+
150
+ if (result.error) throw new Error(result.error.message);
151
+ },
152
+ [],
153
+ );
154
+
155
+ const removeMember = useCallback(async (memberId: string) => {
156
+ const currentOrgId = currentOrgIdRef.current;
157
+ if (!currentOrgId) throw new Error("No active organization");
158
+
159
+ const result = await authClient.organization.removeMember({
160
+ memberIdOrEmail: memberId,
161
+ organizationId: currentOrgId,
162
+ });
163
+
164
+ if (result.error) throw new Error(result.error.message);
165
+ }, []);
166
+
167
+ const updateMemberRole = useCallback(
168
+ async (memberId: string, role: "admin" | "member") => {
169
+ const currentOrgId = currentOrgIdRef.current;
170
+ if (!currentOrgId) throw new Error("No active organization");
171
+
172
+ const result = await authClient.organization.updateMemberRole({
173
+ memberId,
174
+ role,
175
+ organizationId: currentOrgId,
176
+ });
177
+
178
+ if (result.error) throw new Error(result.error.message);
179
+ },
180
+ [],
181
+ );
182
+
183
+ const leaveOrganization = useCallback(async () => {
184
+ const currentOrgId = currentOrgIdRef.current;
185
+ if (!currentOrgId) throw new Error("No active organization");
186
+
187
+ const result = await authClient.organization.leave({
188
+ organizationId: currentOrgId,
189
+ });
190
+
191
+ if (result.error) throw new Error(result.error.message);
192
+ refetch();
193
+ }, [refetch]);
194
+
195
+ const dataValue = useMemo<OrganizationData>(
196
+ () => ({
197
+ currentOrganization,
198
+ organizations,
199
+ currentRole,
200
+ isLoading,
201
+ needsOnboarding,
202
+ }),
203
+ [currentOrganization, organizations, currentRole, isLoading, needsOnboarding],
204
+ );
205
+
206
+ const actionsValue = useMemo<OrganizationActions>(
207
+ () => ({
208
+ switchOrganization,
209
+ createOrganization,
210
+ inviteMember,
211
+ removeMember,
212
+ updateMemberRole,
213
+ leaveOrganization,
214
+ refetch,
215
+ }),
216
+ [
217
+ switchOrganization,
218
+ createOrganization,
219
+ inviteMember,
220
+ removeMember,
221
+ updateMemberRole,
222
+ leaveOrganization,
223
+ refetch,
224
+ ],
225
+ );
226
+
227
+ return { dataValue, actionsValue };
228
+ };
229
+
230
+ export {
231
+ OrganizationDataContext,
232
+ OrganizationActionsContext,
233
+ useOrganizationContext,
234
+ };
235
+
236
+ export type { OrganizationContextValue, OrganizationData, OrganizationActions };
@@ -0,0 +1,45 @@
1
+ // =============================================================================
2
+ // Create Organization Hook
3
+ // =============================================================================
4
+ // SWR mutation hook for creating a new organization and setting it as active.
5
+ //
6
+ // CONFIGURE: Remove if not using the organization plugin.
7
+ // If using TanStack Query instead of SWR, adapt the mutation pattern.
8
+ // =============================================================================
9
+
10
+ import useSWRMutation from "swr/mutation";
11
+ import { authClient } from "../auth-client";
12
+
13
+ export function useCreateOrganization() {
14
+ const { trigger, isMutating, error, reset } = useSWRMutation(
15
+ "create-organization",
16
+ async (_, { arg }: { arg: { name: string } }) => {
17
+ const slug = arg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
18
+
19
+ const result = await authClient.organization.create({
20
+ name: arg.name,
21
+ slug,
22
+ });
23
+
24
+ if (result.error) {
25
+ throw new Error(result.error.message || "Failed to create organization");
26
+ }
27
+
28
+ // Set the new org as active
29
+ if (result.data?.id) {
30
+ await authClient.organization.setActive({
31
+ organizationId: result.data.id,
32
+ });
33
+ }
34
+
35
+ return result.data;
36
+ },
37
+ );
38
+
39
+ return {
40
+ createOrganization: trigger,
41
+ isCreating: isMutating,
42
+ error: error ? (error as Error).message : null,
43
+ reset,
44
+ };
45
+ }
@@ -0,0 +1,26 @@
1
+ // =============================================================================
2
+ // Role-Based Permission Hook
3
+ // =============================================================================
4
+ // Checks if the current user has the required role level.
5
+ // Uses hierarchy: owner (3) > admin (2) > member (1)
6
+ //
7
+ // CONFIGURE: Adjust role hierarchy if your app uses different roles.
8
+ // Remove if not using the organization plugin.
9
+ // =============================================================================
10
+
11
+ import { useOrganizationData } from "./use-organization";
12
+
13
+ export function useHasPermission(
14
+ requiredRole: "owner" | "admin" | "member",
15
+ ): boolean {
16
+ const { currentRole } = useOrganizationData();
17
+
18
+ if (!currentRole) return false;
19
+
20
+ const roleHierarchy = { owner: 3, admin: 2, member: 1 };
21
+ const userLevel =
22
+ roleHierarchy[currentRole as keyof typeof roleHierarchy] ?? 0;
23
+ const requiredLevel = roleHierarchy[requiredRole];
24
+
25
+ return userLevel >= requiredLevel;
26
+ }
@@ -0,0 +1,64 @@
1
+ // =============================================================================
2
+ // Organization Hooks
3
+ // =============================================================================
4
+ // Three hooks for consuming organization context:
5
+ // - useOrganization() — combined data + actions (backward compat)
6
+ // - useOrganizationData() — data only (prevents re-renders from action changes)
7
+ // - useOrganizationActions() — actions only (prevents re-renders from data changes)
8
+ //
9
+ // CONFIGURE: Remove if not using the organization plugin.
10
+ // =============================================================================
11
+
12
+ import { useContext } from "react";
13
+ import {
14
+ OrganizationDataContext,
15
+ OrganizationActionsContext,
16
+ type OrganizationContextValue,
17
+ type OrganizationData,
18
+ type OrganizationActions,
19
+ } from "../context/organization";
20
+
21
+ /**
22
+ * Combined hook — use when you need both data and actions.
23
+ * For better performance, prefer useOrganizationData() or useOrganizationActions().
24
+ */
25
+ const useOrganization = (): OrganizationContextValue => {
26
+ const data = useContext(OrganizationDataContext);
27
+ const actions = useContext(OrganizationActionsContext);
28
+
29
+ if (!data || !actions) {
30
+ throw new Error(
31
+ "useOrganization must be used within an OrganizationProvider",
32
+ );
33
+ }
34
+
35
+ return { ...data, ...actions };
36
+ };
37
+
38
+ /**
39
+ * Data-only hook — prevents re-renders when action functions change.
40
+ */
41
+ export const useOrganizationData = (): OrganizationData => {
42
+ const data = useContext(OrganizationDataContext);
43
+ if (!data) {
44
+ throw new Error(
45
+ "useOrganizationData must be used within an OrganizationProvider",
46
+ );
47
+ }
48
+ return data;
49
+ };
50
+
51
+ /**
52
+ * Actions-only hook — prevents re-renders when data changes.
53
+ */
54
+ export const useOrganizationActions = (): OrganizationActions => {
55
+ const actions = useContext(OrganizationActionsContext);
56
+ if (!actions) {
57
+ throw new Error(
58
+ "useOrganizationActions must be used within an OrganizationProvider",
59
+ );
60
+ }
61
+ return actions;
62
+ };
63
+
64
+ export default useOrganization;
@@ -0,0 +1,21 @@
1
+ // =============================================================================
2
+ // Shared Auth Validation Schemas
3
+ // =============================================================================
4
+ // Base Zod validators used across multiple auth forms (sign-in, sign-up, etc.)
5
+ //
6
+ // CONFIGURE: If using a different validation library, adapt these schemas.
7
+ // =============================================================================
8
+
9
+ import { z } from "zod";
10
+
11
+ export const emailSchema = z
12
+ .string()
13
+ .min(1, "Please enter your email")
14
+ .email("Please enter a valid email address");
15
+
16
+ export const passwordSchema = z
17
+ .string()
18
+ .min(1, "Please enter your password")
19
+ .min(8, "Password must be at least 8 characters");
20
+
21
+ export const nameSchema = z.string().min(1, "Please enter your name").trim();
@@ -0,0 +1,51 @@
1
+ // =============================================================================
2
+ // Organization Form Schemas
3
+ // =============================================================================
4
+ // Zod schemas for organization-related forms: create, update, invite member.
5
+ //
6
+ // CONFIGURE: Remove if not using the organization plugin.
7
+ // =============================================================================
8
+
9
+ import { z } from "zod";
10
+
11
+ export const createOrganizationSchema = z.object({
12
+ name: z
13
+ .string()
14
+ .min(1, "Please enter a workspace name")
15
+ .max(50, "Name must be 50 characters or less")
16
+ .trim(),
17
+ });
18
+
19
+ export type CreateOrganizationFormData = z.infer<
20
+ typeof createOrganizationSchema
21
+ >;
22
+
23
+ export const updateOrganizationSchema = z.object({
24
+ name: z
25
+ .string()
26
+ .min(1, "Please enter an organization name")
27
+ .max(50, "Name must be 50 characters or less")
28
+ .trim(),
29
+ slug: z
30
+ .string()
31
+ .min(1, "Please enter a slug")
32
+ .max(50, "Slug must be 50 characters or less")
33
+ .regex(
34
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
35
+ "Slug must be lowercase letters, numbers, and hyphens",
36
+ )
37
+ .trim(),
38
+ });
39
+
40
+ export type UpdateOrganizationFormData = z.infer<
41
+ typeof updateOrganizationSchema
42
+ >;
43
+
44
+ export const inviteMemberSchema = z.object({
45
+ email: z.string().email("Please enter a valid email address"),
46
+ role: z.enum(["admin", "member"], {
47
+ message: "Please select a role",
48
+ }),
49
+ });
50
+
51
+ export type InviteMemberFormData = z.infer<typeof inviteMemberSchema>;
@@ -0,0 +1,20 @@
1
+ // =============================================================================
2
+ // Organization Types
3
+ // =============================================================================
4
+ // CONFIGURE: Remove if not using the organization plugin.
5
+ // =============================================================================
6
+
7
+ type Organization = {
8
+ id: string;
9
+ name: string;
10
+ slug: string | null;
11
+ logo: string | null;
12
+ createdAt: Date;
13
+ metadata: string | null;
14
+ };
15
+
16
+ type OrganizationWithRole = Organization & {
17
+ role: string;
18
+ };
19
+
20
+ export type { Organization, OrganizationWithRole };
@@ -0,0 +1,184 @@
1
+ // =============================================================================
2
+ // Auth Database Schema — REFERENCE FILE
3
+ // =============================================================================
4
+ // This schema is AUTO-GENERATED by the Better Auth CLI. Do NOT edit manually.
5
+ // To regenerate after changing auth config:
6
+ // npx @better-auth/cli@latest generate --config ./src/auth/index.ts --output ./path/to/schema.ts --yes
7
+ //
8
+ // This file is included as a REFERENCE so you can see the table structure.
9
+ // When scaffolding, generate your own schema from your auth config.
10
+ //
11
+ // Tables:
12
+ // Core: user, session, account, verification
13
+ // Organization: organization, member, invitation (from organization plugin)
14
+ // =============================================================================
15
+
16
+ import { relations } from "drizzle-orm";
17
+ import { boolean, index, pgTable, text, timestamp } from "drizzle-orm/pg-core";
18
+
19
+ export const user = pgTable("user", {
20
+ id: text("id").primaryKey(),
21
+ name: text("name").notNull(),
22
+ email: text("email").notNull().unique(),
23
+ emailVerified: boolean("email_verified").default(false).notNull(),
24
+ image: text("image"),
25
+ createdAt: timestamp("created_at").defaultNow().notNull(),
26
+ updatedAt: timestamp("updated_at")
27
+ .defaultNow()
28
+ .$onUpdate(() => new Date())
29
+ .notNull(),
30
+ currentOrganizationId: text("current_organization_id"),
31
+ });
32
+
33
+ export const session = pgTable(
34
+ "session",
35
+ {
36
+ id: text("id").primaryKey(),
37
+ expiresAt: timestamp("expires_at").notNull(),
38
+ token: text("token").notNull().unique(),
39
+ createdAt: timestamp("created_at").defaultNow().notNull(),
40
+ updatedAt: timestamp("updated_at")
41
+ .$onUpdate(() => new Date())
42
+ .notNull(),
43
+ ipAddress: text("ip_address"),
44
+ userAgent: text("user_agent"),
45
+ userId: text("user_id")
46
+ .notNull()
47
+ .references(() => user.id, { onDelete: "cascade" }),
48
+ activeOrganizationId: text("active_organization_id"),
49
+ },
50
+ (table) => [index("session_userId_idx").on(table.userId)],
51
+ );
52
+
53
+ export const account = pgTable(
54
+ "account",
55
+ {
56
+ id: text("id").primaryKey(),
57
+ accountId: text("account_id").notNull(),
58
+ providerId: text("provider_id").notNull(),
59
+ userId: text("user_id")
60
+ .notNull()
61
+ .references(() => user.id, { onDelete: "cascade" }),
62
+ accessToken: text("access_token"),
63
+ refreshToken: text("refresh_token"),
64
+ idToken: text("id_token"),
65
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
66
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
67
+ scope: text("scope"),
68
+ password: text("password"),
69
+ createdAt: timestamp("created_at").defaultNow().notNull(),
70
+ updatedAt: timestamp("updated_at")
71
+ .$onUpdate(() => new Date())
72
+ .notNull(),
73
+ },
74
+ (table) => [index("account_userId_idx").on(table.userId)],
75
+ );
76
+
77
+ export const verification = pgTable(
78
+ "verification",
79
+ {
80
+ id: text("id").primaryKey(),
81
+ identifier: text("identifier").notNull(),
82
+ value: text("value").notNull(),
83
+ expiresAt: timestamp("expires_at").notNull(),
84
+ createdAt: timestamp("created_at").defaultNow().notNull(),
85
+ updatedAt: timestamp("updated_at")
86
+ .defaultNow()
87
+ .$onUpdate(() => new Date())
88
+ .notNull(),
89
+ },
90
+ (table) => [index("verification_identifier_idx").on(table.identifier)],
91
+ );
92
+
93
+ // Organization plugin tables (remove if not using organization plugin)
94
+
95
+ export const organization = pgTable("organization", {
96
+ id: text("id").primaryKey(),
97
+ name: text("name").notNull(),
98
+ slug: text("slug").notNull().unique(),
99
+ logo: text("logo"),
100
+ createdAt: timestamp("created_at").notNull(),
101
+ metadata: text("metadata"),
102
+ });
103
+
104
+ export const member = pgTable(
105
+ "member",
106
+ {
107
+ id: text("id").primaryKey(),
108
+ organizationId: text("organization_id")
109
+ .notNull()
110
+ .references(() => organization.id, { onDelete: "cascade" }),
111
+ userId: text("user_id")
112
+ .notNull()
113
+ .references(() => user.id, { onDelete: "cascade" }),
114
+ role: text("role").default("member").notNull(),
115
+ createdAt: timestamp("created_at").notNull(),
116
+ },
117
+ (table) => [
118
+ index("member_organizationId_idx").on(table.organizationId),
119
+ index("member_userId_idx").on(table.userId),
120
+ ],
121
+ );
122
+
123
+ export const invitation = pgTable(
124
+ "invitation",
125
+ {
126
+ id: text("id").primaryKey(),
127
+ organizationId: text("organization_id")
128
+ .notNull()
129
+ .references(() => organization.id, { onDelete: "cascade" }),
130
+ email: text("email").notNull(),
131
+ role: text("role"),
132
+ status: text("status").default("pending").notNull(),
133
+ expiresAt: timestamp("expires_at").notNull(),
134
+ createdAt: timestamp("created_at").defaultNow().notNull(),
135
+ inviterId: text("inviter_id")
136
+ .notNull()
137
+ .references(() => user.id, { onDelete: "cascade" }),
138
+ },
139
+ (table) => [
140
+ index("invitation_organizationId_idx").on(table.organizationId),
141
+ index("invitation_email_idx").on(table.email),
142
+ ],
143
+ );
144
+
145
+ // Relations
146
+
147
+ export const userRelations = relations(user, ({ many }) => ({
148
+ sessions: many(session),
149
+ accounts: many(account),
150
+ members: many(member),
151
+ invitations: many(invitation),
152
+ }));
153
+
154
+ export const sessionRelations = relations(session, ({ one }) => ({
155
+ user: one(user, { fields: [session.userId], references: [user.id] }),
156
+ }));
157
+
158
+ export const accountRelations = relations(account, ({ one }) => ({
159
+ user: one(user, { fields: [account.userId], references: [user.id] }),
160
+ }));
161
+
162
+ export const organizationRelations = relations(organization, ({ many }) => ({
163
+ members: many(member),
164
+ invitations: many(invitation),
165
+ }));
166
+
167
+ export const memberRelations = relations(member, ({ one }) => ({
168
+ organization: one(organization, {
169
+ fields: [member.organizationId],
170
+ references: [organization.id],
171
+ }),
172
+ user: one(user, { fields: [member.userId], references: [user.id] }),
173
+ }));
174
+
175
+ export const invitationRelations = relations(invitation, ({ one }) => ({
176
+ organization: one(organization, {
177
+ fields: [invitation.organizationId],
178
+ references: [organization.id],
179
+ }),
180
+ user: one(user, {
181
+ fields: [invitation.inviterId],
182
+ references: [user.id],
183
+ }),
184
+ }));