Team & Sharing: Gap Analysis + P0 Plan

Created Mar 19, 2026 public

Audit of Emberflow's collaboration features — what exists, what's missing, and a concrete plan for the highest-priority gaps.

Current Architecture

graph TB
    subgraph Auth["Better Auth Organization Plugin"]
        ORG[Organization]
        MEM[Member]
        INV[Invitation]
    end

    subgraph Access["Document Access Control"]
        VIS[Visibility<br/>public / private]
        LS[Link Sharing<br/>toggle]
        OA[Org Access<br/>same-org check]
    end

    subgraph UI["Client UI"]
        TM[Team Modal<br/>create org, invite, list members]
        SM[Share Modal<br/>visibility, link sharing, team hint]
    end

    ORG --> MEM
    ORG --> INV
    MEM --> OA
    VIS --> LS
    LS --> OA
    TM --> ORG
    SM --> VIS

What Works Today

Feature Status Notes
Create organization Working Single org per user (UI limitation)
Invite by email Working Sends via Resend, fires and forgets
List members Working Shows email, role badge, remove button
Remove members Working Owner can remove non-owners
Public/private toggle Working Per-document visibility control
Link sharing toggle Working Anyone with URL can view private doc
Org-based access Working Members of same org see private docs

Access Control Flow

graph TD
    subgraph Check["Access Decision"]
        START[User requests<br/>private document] --> PUBLIC{Is doc public?}
        PUBLIC -->|Yes| ALLOW[Allow access]
        PUBLIC -->|No| LINK{Link sharing<br/>enabled?}
        LINK -->|Yes| ALLOW
        LINK -->|No| AUTHED{User logged in?}
        AUTHED -->|No| DENY[Deny — 404]
        AUTHED -->|Yes| AUTHOR{Is author?}
        AUTHOR -->|Yes| ALLOW
        AUTHOR -->|No| SAMEORG{Same org as<br/>author?}
        SAMEORG -->|Yes| ALLOW
        SAMEORG -->|No| DENY
    end

Note: This access check (canAccessPrivateDoc) applies to documents only. Private Spaces check author_id directly and do not respect org membership — a significant gap.

Gap Inventory

P0 — Blocking basic collaboration

# Gap Impact
1 No invitation acceptance flow Invitees get an email with a generic sign-in link. No accept/decline page, no auto-join on sign-in. The invitation table tracks status and expiresAt but nothing reads or updates them. Invitations are fire-and-forget.
2 No pending invitations visibility After sending an invite, the owner has no way to see pending invites, check expiration, resend, or cancel. The data exists in the DB but the UI never queries it.

P1 — Coarse access model

# Gap Impact
3 No per-document sharing Can't share a specific doc with a specific person. It's all-or-nothing: entire org sees all your private docs. No document_shares table exists.
4 No viewer/editor roles Only owner and member roles. Every member gets identical access. Can't give someone read-only or edit-only access.
5 Spaces ignore org membership viewer.js:286 and api.js:749 check author_id only — org members can't access private Spaces even though they can access private docs.

P2 — Polish and management

# Gap Impact
6 Multi-org broken in UI currentOrg = orgs[0] — hardcoded to first org. Schema supports multiple orgs but UI doesn't.
7 No role management Can't change a member's role post-invite.
8 No org settings Can't rename org, update logo, or manage metadata after creation.
9 teamId field unused member and invitation tables have teamId column but nothing references it. Dead schema.

P3 — Nice to have

# Gap Impact
10 No audit trail No tracking of who accessed what, when members joined, or sharing changes.
11 No expiring invitations UI Invites expire per expiresAt but users aren't notified.

P0 Plan: Invitation Acceptance Flow

The invitation email currently links to the generic homepage. The invitee signs in but is never associated with the org. We need an end-to-end flow.

Target Flow

sequenceDiagram
    participant Owner
    participant Server
    participant Email
    participant Invitee

    Owner->>Server: POST /organization/invite-member<br/>{email, role}
    Server->>Server: Create invitation row<br/>status=pending, generate token
    Server->>Email: Send invite email<br/>with accept URL containing invitationId
    Email->>Invitee: "Join {org} on Emberflow"

    Invitee->>Server: GET /invite/{invitationId}
    Server->>Server: Validate invitation<br/>(exists, pending, not expired)

    alt Not signed in
        Server->>Invitee: Render sign-in page<br/>with returnTo=/invite/{id}
        Invitee->>Server: Sign in (OAuth or magic link)
        Server->>Invitee: Redirect to /invite/{id}
    end

    Invitee->>Server: POST /invite/{invitationId}/accept
    Server->>Server: Add member row<br/>Update invitation status=accepted
    Server->>Invitee: Redirect to dashboard<br/>with success toast

Implementation Steps

1. Update invitation email URL (src/auth.js)

The sendInvitationEmail handler currently links to the homepage. Change to include the invitation ID:

{APP_URL}/invite/{invitation.id}

Better Auth's organization plugin generates the invitation row with an id — we just need to use it in the email link.

2. Create invitation acceptance route (src/routes/viewer.js)

New GET /invite/:invitationId route:

3. Create accept API endpoint (src/routes/api.js)

New POST /api/invite/:invitationId/accept:

4. Create invitation acceptance view (src/views/invite.js)

Simple page showing:

P0 Plan: Pending Invitations UI

Target State

The Team modal should show pending invitations between the invite form and the member list.

graph TD
    subgraph TeamModal["Team Modal — Updated"]
        HEADER[Org header + avatar]
        INVITE[Invite form<br/>email + send button]
        PENDING[Pending invitations<br/>email, status, sent date, actions]
        MEMBERS[Member list<br/>email, role, remove button]
    end

    HEADER --> INVITE
    INVITE --> PENDING
    PENDING --> MEMBERS

Implementation Steps

1. Add pending invitations query (src/routes/api.js)

New GET /api/org/:orgId/invitations:

2. Add resend endpoint (src/routes/api.js)

New POST /api/org/:orgId/invitations/:id/resend:

3. Add cancel endpoint (src/routes/api.js)

New DELETE /api/org/:orgId/invitations/:id:

4. Update Team modal HTML (src/views/modals.js)

Add a "Pending invitations" section between invite form and member list:

5. Update Team modal logic (client/scripts/dashboard.js + client/scripts/doc.js)

Key Files to Modify

File Changes
src/auth.js Update sendInvitationEmail URL to include invitation ID
src/routes/viewer.js Add GET /invite/:id route
src/routes/api.js Add accept/decline endpoints, pending invitations CRUD
src/views/modals.js Add pending invitations section to Team modal
src/views/invite.js New — invitation acceptance page
client/scripts/dashboard.js Add pending invitations logic
client/scripts/doc.js Add pending invitations logic (Team modal also exists here)
client/styles/doc.css Styles for pending invitation rows

Database Changes

No new migrations needed — the existing invitation table already has all required fields (id, status, expiresAt, inviterId). We just need to actually use them.

The only potential addition is if we decide email matching should be flexible (allow accepting an invite sent to a different email). In that case we might add a acceptedBy column — but that's a P1 concern.