Repository navigation
Database schema - #52
Merged
Merged
Conversation
Registers the organization plugin on the server and its client counterpart so org-scoped endpoints and actions are available. Schema tables for it land in a follow-up commit.
The `Tickets` identifier was the only mixed-case table; the rest of the schema is lowercase. Aligning it so future migrations and raw SQL references are consistent.
Tables required by the better-auth organization plugin, plus the `activeOrganizationId` column on `session` and the matching drizzle relations. Membership role now lives on `member`, one per org.
Both are superseded: membership is now modelled by the `member` table's per-organization role, and project membership is a relation rather than a single foreign key. Neither column existed in any applied migration, so this is schema-only.
Adds the columns the plugin requires: `role`, `banned`, `banReason`, `banExpires` on `user` and `impersonatedBy` on `session`. `role` is nullable and platform-wide (`user`/`admin`), distinct from the per-organization role on `member`.
Adds a required `organizationId` foreign key with cascade delete and an index, plus the matching drizzle relations on both sides.
Covers organization, member, invitation, project, and tickets. Insert schemas for the auth-owned tables omit the columns Better Auth marks `input: false` so admin/ban state, active organization, and impersonation cannot be set via a validated body.
Adds the organization/member/invitation tables, the admin and active organization columns, and the previously unmigrated project and tickets tables. Entirely additive: no drops or renames, so it applies cleanly to a database at the 0000 baseline.
The script only created a user, so the new organization tables went unexercised. Splits seeding into guarded per-entity steps covering the user, the organization, and the user's owner membership. Each step checks for an existing row first because `member` has no unique index on (organizationId, userId) and an upsert would duplicate on re-run.
…oles Roles and permissions live in shared/access-control.ts so the server and the client derive the same types. Extends the plugin's default statements with `ticket` and `project` resources, keeps each default role's existing grants, and enables dynamic access control so an organization can define extra roles at runtime. Passing `ac`/`roles` to organizationClient() is type-level only -- it is not serialized into requests -- but it must stay in sync with the server options.
Stores the per-organization roles created at runtime as a JSON permission map, with relations and zod schemas. The export name must match the Better Auth model name because the Drizzle adapter resolves tables by model, not by SQL table name.
…ence
Renames:
- `projectName` -> `name`, per the convention of not repeating the
entity name in a column.
- `githubLink` -> `githubRepo`, storing GitHub's `full_name`
(owner/repo) instead of a URL. Every REST endpoint is addressed by
`/repos/{owner}/{repo}` and there is no REST lookup by repository id,
so neither a URL nor a bare id can be used to call the API. The
GraphQL node id is only resolvable via `node(id:)` and must itself be
looked up by name first.
Drops `orgName`, which duplicated the partner's name now that a
partner is reachable by foreign key.
Merges `organizationId` into `projectPartnerId`. The former was a
foreign key and the latter a plain text column describing the same
thing, so a project could name two different partners. The merged
column keeps the domain name and is the single reference, with cascade
delete. The relation key on `projectRelations` becomes
`projectPartner`.
The `organization` table name and export are unchanged: the Better Auth
plugin resolves them by model name, and the Drizzle adapter looks up
`schema[model]`, so renaming would break every organization query.
The organization table represents a project partner, so the seed names it that way. Table and export names are untouched, and a comment on the access control `organization` resource records why that key has to keep the plugin's name.
drizzle-kit 0.31.10 generated the data-copy step for the SQLite table
rebuild with the new column names on both sides:
INSERT INTO __new_project("id", "name", "githubRepo", "projectPartnerId")
SELECT "id", "name", "githubRepo", "projectPartnerId" FROM project
The SELECT reads from the pre-rename table, which has `projectName`
and `githubLink`, so this fails with 'no such column: name'. Corrects
the source side to the old names. The snapshot recorded the renames
correctly, so subsequent generates are unaffected.
Verified against a seeded row: projectName -> name, githubLink ->
githubRepo, projectPartnerId carried over, orgName and organizationId
dropped, foreign key on the merged column.
`projectId` was `text` against `project.id`, an integer primary key, so the reference could not be joined and nothing enforced it. Changes the column to `integer` with a real foreign key, adds an index for the per-project ticket query, and declares the relation on both sides. Cascade delete, matching organization -> project: a ticket has no meaning without its project. `tickets` is declared after `project` so the foreign key resolves in declaration order, consistent with the rest of the file.
SQLite table rebuild. Unlike the rename migration, the data-copy step here is correct as generated: `projectId` keeps the same name on both sides, and INTEGER affinity converts any existing text value. Verified that a stored text '1' comes back as integer 1 after the rebuild.
`tickets.userId` pointed at no table, so a ticket could name a user that does not exist. Adds the foreign key with `set null` and drops NOT NULL: a ticket is project work and outlives the account that submitted it, so deleting a user clears the attribution rather than destroying the ticket. Adds the relation on both sides and an index for per-user ticket lookups. Adds a unique index on `member (organizationId, userId)`. The plugin already rejects a duplicate membership with USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION, so this changes no existing behaviour; it closes the gap where two concurrent requests both pass that check and insert.
The unique index on (organizationId, userId) means the existence check is no longer what keeps a re-run from duplicating the row, so the insert can use onConflictDoNothing and the comment claiming otherwise is gone.
Rebuilds tickets for the nullable set-null userId foreign key and adds the two ticket indexes and the member unique index. The data-copy step is correct as generated: the column names are unchanged on both sides. Verified that a ticket survives its author being deleted with userId nulled, and that a duplicate membership is now rejected.
nuxi writes this to record which module setup versions it resolved, so the file appeared as untracked noise on every run. Tracking it pins the @nuxt/test-utils setup version alongside the dependency in package.json. Note this file is machine-written: if nuxi rewrites it, that surfaces as a diff rather than being silently ignored.
jasonappah
marked this pull request as ready for review
October 1, 2026 20:17
Records which GitHub issue a ticket is linked to, per REQ-F-14. Nullable rather than NOT NULL: a ticket has to exist before it can be linked. REQ-F-09 and REQ-F-13 create tickets in ITS (partner form and embeddable widget), and REQ-F-19 defers a ticket for partner review before forwarding, so requiring an issue at insert time would block those paths. Filled in when the link is made instead. Unique, which SQLite enforces for non-null values while treating NULLs as distinct: unlinked tickets coexist and no two tickets claim the same issue.
Replaces plugin-managed organizations and roles with a plain `user.orgId` relation read by application code. Nothing consumes the plugin endpoints any more. Drops shared/access-control.ts along with them: the `organization`, `member`, and `ac` resource keys existed to gate plugin endpoints, and the ticket/project grants are re-derived from `orgId` instead of a role statement. The client loses `organizationClient` and `adminClient` for the same reason.
…tables Replaces plugin-managed membership with a single nullable `user.orgId` foreign key. A null orgId is the whole role model: null means global admin, set means the user belongs to that partner. Drops `member`, `invitation`, and `organizationRole`. `member` goes because its columns were organizationId, userId, and role -- the first two are now one column on user and the third no longer exists. `invitation` only served the plugin's invite flow, and `organizationRole` only held dynamic access control statements. Drops the plugin-owned columns `user.role`, `user.banned`, `user.banReason`, `user.banExpires`, `session.activeOrganizationId`, and `session.impersonatedBy`, all of which only the removed plugins populated. No onDelete on `orgId`: SET NULL would promote every ex-member to global admin if a partner were deleted, and CASCADE would delete its users, so NO ACTION blocks deleting a partner that still has users. Not unique, since a partner has many users and each user has at most one.
Replaces the member insert with an update of `user.orgId`, which is what makes the seeded user an org user instead of a global admin. The update is skipped when already correct so a re-run stays a no-op.
Drops the member, invitation, and organizationRole tables, adds `user.orgId`, and drops the six plugin-owned columns. Uses ALTER TABLE DROP COLUMN rather than a table rebuild, so there is no data-copy step and no risk of the mapping bug seen in the project rename migration. The one user row survived. Verified that better-auth still resolves its models against the trimmed schema (getSession and signOut both respond), that a partner with users cannot be deleted, that a bogus orgId is rejected, and that an unused partner still can be.
Both were organization-plugin fields: the slug existed for the plugin's URL routing and its unique index, the logo for plugin-managed imagery. Nothing populates or reads either now that the plugin is gone. The seed keyed its idempotency check on slug, so it now looks the partner up by name and the insert carries only a name. Note this makes the seed sensitive to PARTNER_NAME: changing that constant orphans the existing row and the next run adds a second partner, since there is no longer a stable key column to look up by.
Drops the unique index before the column it covered, which SQLite requires. The seeded partner row survived and user.orgId still references organization.id.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #28
Description
This PR sets up the initial schema for tickets and projects. worked on this as a team on a Discord VC on 9/26/2026 then did final review with Tushar during lab on 10/1