Skip to content

feat: Guest user support for groups (without email/account) #685

@Ziesie1

Description

@Ziesie1

Goal

Currently, every group member must have a registered account (email + auth). This creates friction in real-world use cases:

  • You want to track expenses for someone who doesn't use the app (maybe the user will join later)
  • A group member rejoins with a different email address (maybe lost email account) and their expense history is lost
  • You want to use nicknames per group (people often have different names across friend circles)

The goal is to support guest users — participants in a group who don't necessarily have an account, with the option for a real user to later claim and link to them. Similar Issue was also mentioned here #67 and here #667.

I'd like to implement this and am looking for feedback on the right approach before writing code.


Variant A — Simple isGuestUser flag on the existing User model

Add a single boolean field to User:

model User {
  // ...existing fields
  isGuestUser Boolean @default(false)
}

How it works:

  • When adding group members, allow creating a user with just a name (no email), marked as isGuestUser: true
  • On the /join-group page, after authenticating, show a list of unclaimed guest users in the group and offer to claim one
  • On claim: transfer all ExpenseParticipant and GroupUser records from the guest user ID to the real user ID, then delete the guest record

Pros:

  • Minimal schema change
  • Low risk, isolated change
  • All existing logic stays the same

Cons:

  • User identity is still global — no per-group nicknames
  • A person rejoining with a different email still can't reclaim their history without manually claiming the guest
  • Once claimed, the link is permanent (no switching)
  • Doesn't solve the "same person, different email" problem for already-registered users

Variant B — GroupMember as a first-class entity (recommended)

Introduce a GroupMember table that decouples group participation from user accounts:

model GroupMember {
  id        Int     @id @default(autoincrement())
  groupId   Int
  name      String  // nickname, freely chosen per group
  userId    Int?    // nullable — linked real account, or null for unlinked
  group     Group   @relation(...)
  user      User?   @relation(...)

  @@unique([groupId, userId])
}

ExpenseParticipant would reference groupMemberId instead of userId.

How it works:

  • Every person in a group is a GroupMember — with or without an account
  • A GroupMember can have any nickname (e.g. "Max" in one group, "Maxi" in another)
  • A real User account can link to a GroupMember to "claim" it
  • The link can be released or transferred — useful if someone accidentally claims the wrong person
  • A user who rejoins with a different email can still claim their GroupMember and recover full expense history

Pros:

  • Solves all the problems Variant A has
  • Nicknames per group
  • Flexible claim/unclaim/switch — no permanent lock-in
  • Account-independent expense history — the group's data is stable regardless of who has an account
  • More correct mental model: a person in a group is not the same as their online identity

Cons:

  • Bigger schema change — ExpenseParticipant references change from userId to groupMemberId
  • Balance calculations and DB views need to be updated accordingly
  • More implementation effort overall

My preference

Variant B feels like the right long-term design, even though it's more work. Variant A is a quicker win but leaves the core limitation in place.

I'm planning to implement this (or at least attempt it) and would love to know:

  1. Is Variant B the right direction, or is it too invasive for now?
  2. Are there any architectural constraints I should be aware of before starting?

@krokosik Happy to discuss and adjust the approach based on your feedback before writing any code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions