Team & Sharing: Gap Analysis + P0 Plan
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 --> VISWhat 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
endNote: This access check (
canAccessPrivateDoc) applies to documents only. Private Spaces checkauthor_iddirectly 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 toastImplementation 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:
- Query
invitationtable byid - Validate: exists,
status = 'pending',expiresAt > NOW() - If expired/invalid: render error page
- If user not signed in: render sign-in page with
returnToset to current URL - If user signed in: render acceptance page showing org name, inviter, role
3. Create accept API endpoint (src/routes/api.js)
New POST /api/invite/:invitationId/accept:
- Require auth
- Validate invitation (same checks as above)
- Verify invitation email matches signed-in user's email (or allow any — design decision)
- Call Better Auth's organization API to add member, or insert directly into
membertable - Update
invitation.status = 'accepted' - Return success with redirect URL
4. Create invitation acceptance view (src/views/invite.js)
Simple page showing:
- Org name + avatar
- "You've been invited by {inviter} to join {org}"
- Accept / Decline buttons
- If declined: update
status = 'declined', redirect to homepage
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 --> MEMBERSImplementation Steps
1. Add pending invitations query (src/routes/api.js)
New GET /api/org/:orgId/invitations:
- Require auth + verify user is org owner
- Query:
SELECT * FROM invitation WHERE "organizationId" = $1 AND status = 'pending' ORDER BY "createdAt" DESC - Return list with email, role, status, createdAt, expiresAt
2. Add resend endpoint (src/routes/api.js)
New POST /api/org/:orgId/invitations/:id/resend:
- Require auth + verify owner
- Update
expiresAtto new expiry - Re-send invitation email
- Return success
3. Add cancel endpoint (src/routes/api.js)
New DELETE /api/org/:orgId/invitations/:id:
- Require auth + verify owner
- Update
status = 'cancelled' - Return success
4. Update Team modal HTML (src/views/modals.js)
Add a "Pending invitations" section between invite form and member list:
- Section label with clock icon
- List of pending invites: email, "pending" badge, relative time, resend/cancel buttons
- Empty state: hidden when no pending invites
5. Update Team modal logic (client/scripts/dashboard.js + client/scripts/doc.js)
loadPendingInvites()— fetch and render pending invitations- Resend button handler — calls resend endpoint, shows status
- Cancel button handler — calls cancel endpoint, removes row
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.