From 7bbf11fff7e586641cb9804aa0e6a2e7d7086cb9 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Wed, 15 Apr 2026 00:12:59 +0800 Subject: [PATCH] feat(invitation): send email notification when inviting a user Uses the existing Resend email service to notify invitees. Email includes inviter name, workspace name, and a link to the app. Sent fire-and-forget in a goroutine to avoid blocking the API response. --- server/internal/handler/invitation.go | 15 ++++++++++++ server/internal/service/email.go | 34 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/server/internal/handler/invitation.go b/server/internal/handler/invitation.go index 16a580967..104cb9de6 100644 --- a/server/internal/handler/invitation.go +++ b/server/internal/handler/invitation.go @@ -134,11 +134,26 @@ func (h *Handler) CreateInvitation(w http.ResponseWriter, r *http.Request) { // Notify the invitee in real time if they are a registered user. userID := requestUserID(r) eventPayload := map[string]any{"invitation": resp} + var workspaceName string if ws, err := h.Queries.GetWorkspace(r.Context(), parseUUID(workspaceID)); err == nil { + workspaceName = ws.Name eventPayload["workspace_name"] = ws.Name } h.publish(protocol.EventInvitationCreated, workspaceID, "member", userID, eventPayload) + // Send invitation email (fire-and-forget). + if h.EmailService != nil && workspaceName != "" { + inviterName := email // fallback + if inviter, err := h.Queries.GetUser(r.Context(), requester.UserID); err == nil { + inviterName = inviter.Name + } + go func() { + if err := h.EmailService.SendInvitationEmail(email, inviterName, workspaceName); err != nil { + slog.Warn("failed to send invitation email", "email", email, "error", err) + } + }() + } + writeJSON(w, http.StatusCreated, resp) } diff --git a/server/internal/service/email.go b/server/internal/service/email.go index 7ce61d4f0..e6b4d81ff 100644 --- a/server/internal/service/email.go +++ b/server/internal/service/email.go @@ -3,6 +3,7 @@ package service import ( "fmt" "os" + "strings" "github.com/resend/resend-go/v2" ) @@ -52,3 +53,36 @@ func (s *EmailService) SendVerificationCode(to, code string) error { _, err := s.client.Emails.Send(params) return err } + +// SendInvitationEmail notifies the invitee that they have been invited to a workspace. +func (s *EmailService) SendInvitationEmail(to, inviterName, workspaceName string) error { + // Build the app URL for the invitation — users will see pending invitations + // in the workspace switcher after logging in. + appURL := strings.TrimSpace(os.Getenv("FRONTEND_ORIGIN")) + if appURL == "" { + appURL = "https://app.multica.ai" + } + + if s.client == nil { + fmt.Printf("[DEV] Invitation email to %s: %s invited you to %s — %s\n", to, inviterName, workspaceName, appURL) + return nil + } + + params := &resend.SendEmailRequest{ + From: s.fromEmail, + To: []string{to}, + Subject: fmt.Sprintf("%s invited you to %s on Multica", inviterName, workspaceName), + Html: fmt.Sprintf( + `
+

You're invited to join %s

+

%s invited you to collaborate in the %s workspace on Multica.

+

+ Open Multica +

+

Log in to accept or decline the invitation.

+
`, workspaceName, inviterName, workspaceName, appURL), + } + + _, err := s.client.Emails.Send(params) + return err +}