Skip to content

ADR-0001. Unsubscribe theo token (không auth, không session)

FieldValue
StatusAccepted
Date2026-05-01
Decidersoutreach-team
Supersedes

Bối cảnh

  • Người nhận newsletter phải có thể unsubscribe từ một link một-click trong email, mà không cần đăng nhập.
  • Một subscriber là một actor công khai, không xác thực — không có JWT, không có session, không có merchant scope.
  • Chúng tôi cần một cách để cấp quyền "deactivate đúng subscription này" an toàn để nhúng vào một URL email plaintext và không thể dùng để enumerate hay deactivate subscription của người khác.

Quyết định

Mỗi dòng Subscriber mang một unsubscribeToken, sinh qua IdGenerator.nextId() (một Snowflake 64-bit) khi insert. Link unsubscribe là GET /subscribers/unsubscribe?token=<unsubscribeToken> và yêu cầu không xác thực.

SubscriberService.unsubscribe() tra cứu token; trúng thì set status=DEACTIVATED và đóng dấu unsubscribedAt; trượt thì throw UNSUBSCRIBE_INVALID_TOKEN (HTTP 404). Token được khai báo trong hiddenProperties của model, nên không bao giờ được trả về bởi bất kỳ endpoint đọc nào — cách duy nhất để có nó là nhận email.

Hệ quả

ƯuNhược
Unsubscribe một-click với zero auth frictionToken là một bearer secret trong URL email plaintext
Token không bao giờ rò qua API (hidden property)Không expiry / rotation — một link rò hoạt động vô thời hạn
Không gian Snowflake (64-bit) làm việc đoán bất khả thiRe-subscribe không phát token mới (token ổn định qua deactivate/reactivate)
Idempotent: hit lại link trên một dòng đã-deactivate là vô hại404 trên token sai hơi rò "token không tồn tại"

Các phương án đã cân nhắc

Phương ánƯuNhượcLý do loại bỏ
JWT ký trong linkExpiry tích hợp, không cần cột DBCần quản lý key; URL dài xấu; subscriber không có account để scopeQuá mức cho một list công khai
Email + bước xác nhậnVerify quyền sở hữuThêm friction; phá vỡ kỳ vọng một-clickHại tỷ lệ hoàn tất unsubscribe (và ý định pháp lý một-click)
Token UUID ngẫu nhiênCùng UXCần generator riêng; Snowflake đã có sẵn qua IdGeneratorSnowflake tái dùng hạ tầng sẵn có

Tham chiếu

  • packages/core/src/models/schemas/outreach/subscriber/schema.ts (unsubscribeToken $defaultFn, hiddenProperties)
  • packages/outreach/src/services/subscriber.service.ts (unsubscribe)
  • packages/outreach/src/errors/subscriber.errors.ts (UNSUBSCRIBE_INVALID_TOKEN)
  • packages/outreach/src/controllers/subscriber/definitions.ts (route UNSUBSCRIBE)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.