-
-
Notifications
You must be signed in to change notification settings - Fork 439
✨ Add spaces concept #1776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Add spaces concept #1776
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Skipped Deployment
|
WalkthroughThis change introduces a new "space" entity to the application, updating the database schema, Prisma models, and business logic to associate users, polls, subscriptions, and scheduled events with spaces. User creation now automatically generates a personal space. Subscription checkout, webhook handlers, poll creation, guest merging, and seeding logic are updated to handle and link the new space entity. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant API
participant DB
participant Stripe
User->>API: Register / Sign up
API->>DB: Create User with "Personal" Space
API->>User: Return User & Space info
User->>API: Create Poll
API->>DB: Get Active Space for User
API->>DB: Create Poll with spaceId
API->>User: Return Poll
User->>API: Start Subscription (Stripe)
API->>DB: Get Active Space for User
API->>Stripe: Create Checkout Session (metadata includes spaceId)
Stripe->>API: Webhook: Subscription Created
API->>DB: Create Subscription with spaceId
Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate Unit Tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🔭 Outside diff range comments (1)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (1)
52-68
: 🛠️ Refactor suggestionAvoid mutating the primary key – prefer an
upsert
prisma.subscription.update
changes theid
column (the PK) of the existing row.
Updating PKs is usually discouraged: it can break FK references and performs worse than anupsert
. Anupsert
keyed onuserId
(or the existing PK) keeps the PK stable and expresses intent more clearly.Consider:
await prisma.subscription.upsert({ where: { id: subscription.id }, create: { id: subscription.id, userId, spaceId, /* remaining fields */ }, update: { active: isActive, priceId, /* remaining fields except `id` */ }, });
🧹 Nitpick comments (9)
packages/database/prisma/migrations/20250614110551_create_spaces/migration.sql (2)
1-6
:ON CONFLICT DO NOTHING
is ineffective hereThe inserted
id
is a fresh UUID for every row, so a primary-key clash can’t happen.
If the intention is to avoid duplicates per-user, add a UNIQUE constraint on(owner_id, name)
and target that in theON CONFLICT (…) DO NOTHING
clause; otherwise remove the clause for clarity.
8-17
: Potential multi-row join ambiguity
UPDATE … FROM spaces
will arbitrarily pick one of the user’s spaces if they already had >1 (not covered by the earlier insert).
Add a deterministic filter (e.g.spaces.name = 'Personal'
) or use a sub-query selecting the first space per owner to guarantee consistent results.apps/web/src/auth/helpers/merge-user.ts (1)
18-21
: Log message is misleading – it’s the space that is missing, not the user.- console.warn(`User ${userId} not found`); + console.warn(`Space for user ${userId} not found`);A precise message is invaluable when diagnosing production issues.
packages/database/prisma/models/poll.prisma (1)
49-51
: Add an index onspaceId
for large-scale querying.Queries like “all polls in a space” will now be common. An index keeps them O(log n) instead of full-table scans.
@@index([spaceId])
Place it near the other
@@index
declarations.apps/web/src/app/api/stripe/checkout/route.ts (1)
118-126
: Type assertions hide runtime mismatches – prefer explicit casts.
... satisfies SubscriptionCheckoutMetadata
is compile-time only. IfspaceId
ever comes through asundefined
(e.g. race condition above), Stripe still receives it and the database update later fails. Add a runtime guard just before building the payload:if (!spaceId) { return NextResponse.json({ error: "No space found" }, 400); }apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (1)
21-26
: Surface Zod validation errors instead of a generic messageToday any schema‐validation failure is reported as “Missing user ID”, which makes debugging painful and can mask other issues (e.g. malformed
spaceId
). Bubble up the Zod error messages so we know exactly what is wrong.- if (!res.success) { - throw new Error("Missing user ID"); - } + if (!res.success) { + throw new Error( + `Invalid subscription metadata – ${res.error.errors + .map((e) => e.message) + .join(", ")}` + ); + }apps/web/src/features/subscription/schema.ts (1)
4-6
: ValidatespaceId
format early withz.string().uuid()
Space.id
is generated withuuid()
, so accepting arbitrary strings here postpones errors until DB write time. Tighten the schema:- spaceId: z.string().optional(), + spaceId: z.string().uuid().optional(),Apply the same change to
subscriptionMetadataSchema
.Also applies to: 13-15
packages/database/prisma/models/user.prisma (2)
61-62
: Add a uniqueness guard on space names per userIf users can create multiple spaces, duplicate names will be confusing. A composite constraint prevents that:
@@unique([ownerId, name])
Place it inside
model Space
.
70-82
: Align ID generation strategy across models
User
/Poll
usecuid()
/uuid()
inconsistently. Consider standardising on one (usuallycuid()
for Prisma) to avoid cognitive overhead and mixed formats in URLs & logs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
apps/web/src/app/api/stripe/checkout/route.ts
(4 hunks)apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts
(3 hunks)apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/updated.ts
(1 hunks)apps/web/src/app/api/stripe/webhook/handlers/utils.ts
(0 hunks)apps/web/src/auth/adapters/prisma.ts
(1 hunks)apps/web/src/auth/helpers/merge-user.ts
(2 hunks)apps/web/src/features/subscription/schema.ts
(1 hunks)apps/web/src/trpc/routers/auth.ts
(1 hunks)apps/web/src/trpc/routers/polls.ts
(2 hunks)packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql
(1 hunks)packages/database/prisma/migrations/20250614110551_create_spaces/migration.sql
(1 hunks)packages/database/prisma/models/billing.prisma
(1 hunks)packages/database/prisma/models/poll.prisma
(1 hunks)packages/database/prisma/models/user.prisma
(1 hunks)packages/database/prisma/seed/users.ts
(3 hunks)
💤 Files with no reviewable changes (1)
- apps/web/src/app/api/stripe/webhook/handlers/utils.ts
🧰 Additional context used
🧠 Learnings (1)
apps/web/src/auth/helpers/merge-user.ts (1)
Learnt from: lukevella
PR: lukevella/rallly#1189
File: apps/web/src/utils/auth.ts:202-202
Timestamp: 2024-07-03T10:52:47.959Z
Learning: The `mergeGuestsIntoUser()` function in `apps/web/src/utils/auth.ts` is correctly awaited.
🧬 Code Graph Analysis (3)
apps/web/src/auth/adapters/prisma.ts (1)
apps/web/src/trpc/routers/user.ts (1)
user
(25-248)
apps/web/src/app/api/stripe/checkout/route.ts (2)
apps/web/src/trpc/routers/user.ts (1)
user
(25-248)apps/web/src/features/subscription/schema.ts (2)
SubscriptionCheckoutMetadata
(8-10)SubscriptionMetadata
(17-17)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (1)
apps/web/src/app/api/stripe/webhook/handlers/utils.ts (1)
toDate
(6-8)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Integration tests
🔇 Additional comments (7)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/updated.ts (1)
5-6
: Schema import centralisation looks correctMoving
subscriptionMetadataSchema
to the dedicated feature module keeps all subscription-related validation in one place. No further action required.apps/web/src/trpc/routers/polls.ts (1)
250-251
:spaceId
may beundefined
for non-guest usersThe Prisma schema most likely marks
spaceId
as non-nullable whenuserId
is set. Passingundefined
will therefore explode at runtime.
After enforcing the fail-fast logic above this line becomes safe, otherwise wrap it in a non-optional assignment.packages/database/prisma/seed/users.ts (2)
14-19
: Seeder aligns with new modelCreating a personal space for each seeded user keeps the dev DB consistent with production rules.
LGTM.
30-35
: Good linkage of subscription → spaceExplicitly setting
spaceId
on the pro user’s subscription prevents NULLs after back-fills. No issues spotted.Also applies to: 47-48
packages/database/prisma/models/billing.prisma (1)
14-17
: Double-uniqueness (userId
andspaceId
) may be overly restrictive.
userId @unique
already guarantees one subscription per user.
AddingspaceId @unique
means a user cannot have two subscriptions across any of their spaces and two different users cannot subscribe to the same shared space. Confirm this is intentional – it blocks:• Multiple pro spaces under one account
• Organisation-style shared spaces with separate ownersIf the goal is merely a FK, drop
@unique
and add@@index([spaceId])
.apps/web/src/app/api/stripe/checkout/route.ts (1)
99-100
: First-space heuristic mirrors earlier brittleness.If users create multiple spaces the ordering from Prisma is undefined.
Consider the same deterministic or validated selection strategy suggested inmerge-user.ts
.packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql (1)
24-26
: Deployment safety: ensure no duplicatespace_id
before adding the unique indexThe script warns about duplicates but doesn’t guard against them. Run a pre-migration check (or create the index
NOT VALID
, validate afterwards) to avoid runtime failures on production deploys.
Example verification query:SELECT space_id, COUNT(*) FROM subscriptions WHERE space_id IS NOT NULL GROUP BY space_id HAVING COUNT(*) > 1;
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (7)
apps/web/src/features/spaces/queries.ts (1)
3-11
: Consider adding a stable ordering & minimal select.
findMany
without anorderBy
returns results in an undefined order.
If callers rely on a deterministic list (e.g. first element is “personal” space) you can introduce subtle bugs later.
Also, if only a subset of fields is actually consumed, adding aselect
clause prevents over-fetching.-const spaces = await prisma.space.findMany({ - where: { ownerId }, -}); +const spaces = await prisma.space.findMany({ + where: { ownerId }, + orderBy: { createdAt: "asc" }, + select: { + id: true, + name: true, // keep only what callers need + createdAt: true, + }, +});packages/database/prisma/models/event.prisma (2)
20-23
: Missing index onspaceId
may hurt query performance.Scheduled events are now queryable per space (UI filters, cascade deletes, etc.).
Add an index to line-haul look-ups:@@index([spaceId])
34-36
: Potential data-integrity gap betweenuserId
andspaceId
.Nothing prevents inserting a
ScheduledEvent
whoseuserId
belongs to Alice while itsspaceId
belongs to Bob.
If that should never happen, enforce a composite check in application code or add a DB-level constraint (e.g. trigger) tyingspace.ownerId
toscheduled_events.user_id
.packages/database/prisma/seed/polls.ts (2)
71-76
:space.connect
can short-circuit when seeding huge datasets.Each
poll.create
executes an extra FK lookup. When seeding thousands of polls you’ll notice latency.
Minor optimisation: passspaceId
directly instead of nested connect.- space: { - connect: { id: spaceId }, - }, + spaceId,
122-130
: Early failure on missing space is 👍 – maybe include user email for easier debugging.Adding the extra context helps SREs grep logs without joining tables.
throw new Error(`No space found for user ${userId} while seeding polls`);packages/database/prisma/seed/scheduled-events.ts (2)
57-63
: Return the created event (optional).
createScheduledEventForUser
currently returnsvoid
, yet the caller aggregates promises (Promise.all
).
Returning theprisma.scheduledEvent.create
promise allows typed consumers and future callers to access the created entity without additional queries.-async function createScheduledEventForUser({ +async function createScheduledEventForUser({ userId, spaceId, }: { userId: string; spaceId: string; -}) { +}): Promise<Awaited<ReturnType<typeof prisma.scheduledEvent.create>>> { … - await prisma.scheduledEvent.create({ + return prisma.scheduledEvent.create({ data: { … }, }); }
148-156
: Selecting the first space can be ambiguous.If a user owns multiple spaces,
findFirst
returns an arbitrary one.
Consider adding deterministic ordering (e.g.,orderBy: { createdAt: "asc" }
) or filtering for the “Personal” space explicitly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
apps/web/src/auth/helpers/merge-user.ts
(3 hunks)apps/web/src/auth/queries.ts
(2 hunks)apps/web/src/features/spaces/queries.ts
(1 hunks)apps/web/src/trpc/routers/polls.ts
(6 hunks)packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql
(1 hunks)packages/database/prisma/migrations/20250614115818_migrate_events_to_spaces/migration.sql
(1 hunks)packages/database/prisma/models/event.prisma
(2 hunks)packages/database/prisma/models/user.prisma
(1 hunks)packages/database/prisma/seed/polls.ts
(3 hunks)packages/database/prisma/seed/scheduled-events.ts
(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/web/src/auth/helpers/merge-user.ts
- apps/web/src/trpc/routers/polls.ts
- packages/database/prisma/models/user.prisma
- packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web/src/auth/queries.ts (3)
apps/web/src/next-auth.config.ts (1)
session
(24-32)apps/web/src/features/user/queries.ts (1)
getUser
(6-45)apps/web/src/features/spaces/queries.ts (1)
getDefaultSpace
(13-21)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Integration tests
🔇 Additional comments (4)
apps/web/src/features/spaces/queries.ts (1)
13-21
:getDefaultSpace
should deterministically pick one space.
findFirst
without anorderBy
can return any matching row depending on the underlying query-plan.
Use an explicit ordering (e.g. earliest creation) to avoid flakiness, especially in unit tests & migrations that rely on “first space”.-const space = await prisma.space.findFirst({ - where: { ownerId }, -}); +const space = await prisma.space.findFirst({ + where: { ownerId }, + orderBy: { createdAt: "asc" }, +});packages/database/prisma/seed/polls.ts (1)
44-50
: Good parameter refactor – reduces arg-order bugs.
Moving to a single object argument eliminates the classic(userId, spaceId)
swap mistake. Nice!packages/database/prisma/seed/scheduled-events.ts (2)
3-3
: Remove superfluous comment.
// Ensure Prisma is imported
does not add value and clutters the import section.
[ suggest_nitpick ]-import { ScheduledEventStatus } from "@prisma/client"; // Ensure Prisma is imported +import { ScheduledEventStatus } from "@prisma/client";
104-108
:❓ Verification inconclusive
Verify enum casing matches the schema.
ScheduledEventStatus.confirmed
/canceled
assume lower-case enum members.
Prisma often generates Pascal-cased values (CONFIRMED
,CANCELED
). A mismatch will surface at runtime.Run a quick grep to confirm the generated enum literals:
🏁 Script executed:
#!/bin/bash # Inspect generated Prisma client enum values fd --extension ts | xargs grep -n "enum ScheduledEventStatus" -A 5 -B 0Length of output: 73
🏁 Script executed:
#!/bin/bash # Search for generated Prisma Client enum definitions to confirm member casing rg "export enum ScheduledEventStatus" -n -A5 -B2Length of output: 50
🏁 Script executed:
#!/bin/bash # Locate Prisma schema and print ScheduledEventStatus enum definition schema_file=$(fd -t f -e prisma schema.prisma | head -n1) if [[ -n "$schema_file" ]]; then sed -n '/^enum ScheduledEventStatus/,/^}/p' "$schema_file" else echo "schema.prisma not found" fiLength of output: 234
🏁 Script executed:
#!/bin/bash # List all enum definitions in the Prisma schema grep -n "enum " packages/database/prisma/schema.prisma # Locate all references to ScheduledEventStatus in the schema grep -n "ScheduledEventStatus" packages/database/prisma/schema.prismaLength of output: 126
Verify Prisma enum casing for ScheduledEventStatus
The generated Prisma enum values for ScheduledEventStatus may be defined in ALL_CAPS (e.g. CONFIRMED, CANCELED) while the seed script uses lowercase members. A mismatch will surface at runtime.Please confirm in your Prisma schema (packages/database/prisma/schema.prisma) that ScheduledEventStatus is declared with the same casing, and update the seed code accordingly, for example:
const status = faker.helpers.arrayElement<ScheduledEventStatus>([ ScheduledEventStatus.CONFIRMED, ScheduledEventStatus.CANCELED, ]);• File: packages/database/prisma/seed/scheduled-events.ts
• Lines: 104–108
packages/database/prisma/migrations/20250614115818_migrate_events_to_spaces/migration.sql
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (1)
74-91
: 🛠️ Refactor suggestionUpdating the primary-key
id
may break referential integrity
prisma.subscription.update({ where: { id: existingUser.subscription.id }, data: { id: subscription.id, … } })
mutates the primary key of the row.
PostgreSQL allows it, but:
- It requires
ON UPDATE CASCADE
FKs – check your schema.- Concurrent webhook deliveries could race: another handler might already have created a row with the new
subscription.id
, leading to a unique-constraint violation.Consider an UPSERT keyed by the incoming
subscription.id
instead:-await prisma.subscription.update({ - where: { id: existingUser.subscription.id }, - data: { - id: subscription.id, - // … - }, -}); +await prisma.subscription.upsert({ + where: { id: subscription.id }, + create: { + id: subscription.id, + userId, + spaceId, + // …other fields + }, + update: { + active: isActive, + priceId, + currency, + interval, + amount, + status: subscription.status, + periodStart: toDate(subscription.current_period_start), + periodEnd: toDate(subscription.current_period_end), + cancelAtPeriodEnd: subscription.cancel_at_period_end, + spaceId, + }, +});This removes the PK rewrite and is naturally idempotent.
🧹 Nitpick comments (2)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (2)
31-36
: Narrow the Prismaselect
to avoid pulling the entire subscription object
select: { subscription: true }
materialises all columns of the related subscription even though only itsid
is used later (existingUser.subscription.id
).
Reducing the projection trims network I/O and memory, especially once the subscription table grows.- select: { - subscription: true, - }, + select: { + subscription: { + select: { + id: true, + }, + }, + },
95-110
: Deduplicate thedata
object between the update & create pathsBoth branches build nearly identical field lists. Extract them once to keep future field additions (e.g.
trialEnd
) DRY:const baseData = { id: subscription.id, userId, spaceId, active: isActive, priceId, currency, interval, amount, status: subscription.status, createdAt: toDate(subscription.created), periodStart: toDate(subscription.current_period_start), periodEnd: toDate(subscription.current_period_end), cancelAtPeriodEnd: subscription.cancel_at_period_end, };Then spread
baseData
into eitherupdate
orcreate
.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts
(3 hunks)apps/web/src/features/spaces/queries.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/src/features/spaces/queries.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (2)
apps/web/src/features/spaces/queries.ts (2)
getSpace
(30-38)getDefaultSpace
(13-28)apps/web/src/app/api/stripe/webhook/handlers/utils.ts (1)
toDate
(6-8)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Integration tests
🔇 Additional comments (1)
apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts (1)
44-60
: Great to see the ownership validation added!The explicit check that the
spaceId
belongs to the user closes the privilege-escalation hole raised in the previous review.
✔️ Reads well and mirrors the fallback path.
Summary by CodeRabbit