·

Tiếng Việt: Hands On Writing A Production Quality Technical Spec

Hands On Writing A Production Quality Technical Spec

Một spec (đặc tả kỹ thuật) tốt không phải là tài liệu bạn viết cho có — nó là bản thiết kế quyết định agent sẽ tạo ra code chất lượng production hay một mớ hỗn độn cần sửa lại từ đầu.

Vì Sao Spec Là Artifact Có Đòn Bẩy Cao Nhất Trong Agentic Development

Khi làm việc với AI coding agent, có một sự thật mà nhiều kỹ sư mất khá lâu mới nhận ra: chất lượng output của agent gần như tỷ lệ thuận với chất lượng input bạn đưa cho nó. Và input quan trọng nhất, có sức ảnh hưởng lớn nhất, chính là spec.

Hãy thử làm một phép so sánh chi phí thực tế. Giả sử bạn cần triển khai một tính năng xác thực (authentication) mới. Có hai con đường:

Con đường 1 — prompt nhanh, không spec: Bạn gõ một câu prompt vài dòng kiểu "thêm magic link login cho app", để agent tự suy luận phần còn lại. Agent sẽ đoán: đoán token hết hạn sau bao lâu, đoán có cần rate limit không, đoán xử lý ra sao khi user bấm link cũ, đoán format response API. Xác suất cao là bạn sẽ phải review lại toàn bộ, phát hiện thiếu edge case, yêu cầu sửa, agent sửa sai chỗ khác, rồi lặp lại vòng lặp này 4-5 lần. Mỗi vòng lặp tốn thời gian context-switch của bạn, tốn token, và quan trọng hơn — tốn niềm tin của team vào việc dùng agent cho việc nghiêm túc.

Con đường 2 — đầu tư 30-45 phút viết spec: Bạn dành thời gian viết rõ ràng bối cảnh, yêu cầu chức năng, edge case, tiêu chí nghiệm thu. Agent đọc spec, thực thi đúng theo đặc tả, và vì mọi quyết định mơ hồ đã được bạn quyết định trước — nó không cần đoán. Review của bạn chuyển từ "tìm xem thiếu gì" sang "kiểm tra xem code có khớp spec không" — nhanh hơn nhiều và ít burn-out hơn.

Chênh lệch chi phí này nhân lên theo ba trục:

1. Tái sử dụng qua nhiều lần chạy (rework qua các agent run). Một spec tốt là tài sản dùng lại được. Bạn có thể chạy cùng một spec qua nhiều agent khác nhau, hoặc chạy lại khi lần đầu agent đi sai hướng, mà không cần viết lại ngữ cảnh từ đầu. Không có spec, mỗi lần retry là một lần bạn phải nhớ lại và gõ lại toàn bộ ngữ cảnh trong đầu.

2. Tính nhất quán qua nhiều lần chạy. LLM không tất định (non-deterministic) một cách tuyệt đối — cùng một prompt mơ hồ có thể ra hai kết quả khác nhau ở hai lần chạy. Một spec chi tiết, rõ ràng thu hẹp không gian output có thể chấp nhận được, khiến kết quả ổn định hơn giữa các lần chạy, giữa các agent khác nhau (Claude, một agent khác, hay một kỹ sư con người mới join).

3. Tốc độ review và onboarding. Reviewer — dù là con người hay agent khác đóng vai reviewer — làm việc nhanh hơn rất nhiều khi có acceptance criteria rõ ràng để đối chiếu, thay vì phải tự suy luận "tính năng này lẽ ra phải hoạt động thế nào". Với kỹ sư mới hoặc agent mới được giao việc tiếp tục một tính năng dở dang, spec chính là tài liệu onboarding nhanh nhất — nhanh hơn nhiều so với đọc code và đoán ý định ban đầu.

Nói cách khác: thời gian bạn "mất" để viết spec không phải là chi phí phát sinh thêm — nó là chi phí bạn vốn dĩ phải trả (cho việc suy nghĩ thấu đáo về tính năng), chỉ là bạn trả trước thay vì trả sau dưới dạng rework, bug production, và review kéo dài.

Mẹo: Nếu bạn thấy mình đang review lại cùng một loại lỗi (thiếu rate limit, thiếu xử lý lỗi mạng, sai định dạng response) ở nhiều tính năng khác nhau do agent làm ra — đó là dấu hiệu spec của bạn đang thiếu một mục cố định nào đó. Hãy thêm nó thành checklist chuẩn cho mọi spec sau này.

Cấu Trúc Của Một Spec Hiệu Quả Với Agent

Một spec chất lượng production không cần dài dòng, nhưng cần đầy đủ sáu thành phần cốt lõi. Thiếu bất kỳ thành phần nào, agent sẽ phải tự suy luận — và suy luận của agent không phải lúc nào cũng khớp ý bạn.

Context (Bối cảnh). Đây là phần mô tả hệ thống hiện tại: kiến trúc, stack công nghệ, các convention đang dùng, các file/module liên quan. Agent không có trí nhớ về codebase của bạn ngoài những gì nó đọc được — context là nơi bạn "cấy" hiểu biết nền tảng đó vào, giảm khả năng nó đề xuất một pattern xa lạ với codebase.

Problem Statement (Phát biểu vấn đề). Mô tả rõ vấn đề đang cần giải quyết và tại sao — không phải giải pháp. Một problem statement tốt trả lời: ai gặp vấn đề này, vấn đề hiện tại gây ra hậu quả gì, và ranh giới của vấn đề (scope) ở đâu để tránh scope creep.

Functional Requirements (Yêu cầu chức năng). Danh sách cụ thể, có thể kiểm chứng, về việc hệ thống phải làm gì. Đây là phần "xương sống" agent sẽ bám vào để sinh code — càng cụ thể, càng ít chỗ cho agent tự chế.

Non-Functional Requirements (Yêu cầu phi chức năng). Các ràng buộc về hiệu năng, bảo mật, khả năng mở rộng, độ tin cậy. Đây là phần dễ bị bỏ quên nhất khi viết spec vội, nhưng lại chính là phần gây ra sự cố production nghiêm trọng nhất khi thiếu (ví dụ: quên yêu cầu hash token trước khi lưu DB).

Edge Cases and Error Handling (Trường hợp biên và xử lý lỗi). Liệt kê các tình huống bất thường: input sai, race condition, tài nguyên không tồn tại, timeout. Đây là phần mà AI thực sự tỏa sáng khi đóng vai trò brainstorm partner — nó có thể quét qua hàng loạt edge case dựa trên pattern đã thấy ở hàng triệu codebase khác.

Acceptance Criteria (Tiêu chí nghiệm thu). Định nghĩa "xong" nghĩa là gì, thường viết theo dạng Given/When/Then. Đây là cầu nối giữa spec và test — một spec tốt có thể gần như copy thẳng acceptance criteria thành test case.

Phần thực hành bên dưới sẽ đi qua từng thành phần này theo đúng thứ tự, áp dụng lên một tính năng thực tế: magic link authentication (xác thực bằng liên kết đăng nhập không cần mật khẩu).

Mẹo: Đừng viết sáu phần này theo thứ tự tuyến tính một lần rồi xong. Thứ tự thực hành hiệu quả nhất thường là: Context → Problem → Functional Requirements nháp → dùng AI để tìm edge case → hoàn thiện Edge Cases → Acceptance Criteria → review lại toàn bộ → thêm Non-Functional Requirements. Vòng lặp qua lại giữa các phần giúp bạn phát hiện mâu thuẫn sớm.

Giả định kỹ thuật cho bài thực hành này: backend Node.js/Express với TypeScript, ORM là Prisma, database PostgreSQL, đã có bảng users và hệ thống session dựa trên cookie (session lưu server-side, ví dụ Redis). Email được gửi qua một service như SendGrid hoặc AWS SES thông qua một module emailService nội bộ. Đây là giả định hợp lý cho một web app B2B/B2C điển hình — nếu stack của bạn khác, các nguyên tắc dưới đây vẫn áp dụng nguyên vẹn, chỉ thay đổi chi tiết công nghệ.

Tính năng cần xây: cho phép user đăng nhập bằng cách nhập email, nhận một liên kết ("magic link") qua email, bấm vào để đăng nhập mà không cần mật khẩu.

Step 1: Thu Thập Context Từ Codebase Hiện Có

Trước khi viết một dòng spec nào, bạn cần hiểu hệ thống auth hiện tại đang hoạt động ra sao. Đừng đoán — hãy đọc thật. Các điểm cần khảo sát:

  • Route auth hiện có (/auth/login, /auth/register) — nằm ở đâu, dùng middleware nào, validate input bằng thư viện gì (Zod? Joi?).
  • Model User trong schema Prisma — các trường hiện có, có trường nào liên quan token/session chưa.
  • Cách session hiện tại được tạo, lưu, và gắn vào cookie — cookie có httpOnly, secure, sameSite không.
  • Module gửi email hiện có — interface của nó, có template engine không, có retry logic khi gửi thất bại không.
  • Middleware rate-limiting hiện có (nếu có) — dùng express-rate-limit hay tự viết.

Thay vì tự đọc từng file, bạn có thể giao việc này cho AI agent với một prompt rõ ràng để nó tổng hợp lại cho bạn:

I'm about to write a spec for adding magic link authentication
to this Express + TypeScript + Prisma application. Before I write
the spec, summarize the existing relevant code so I have accurate
context:

1. List all existing auth-related routes and briefly describe what
   each does (file path + route + purpose).
2. Show the current Prisma `User` model schema.
3. Describe how sessions are currently created and stored (cookie
   settings, storage backend, TTL if any).
4. Describe the interface of the existing email-sending service
   (function signature, how templates work, error handling behavior).
5. Note any existing rate-limiting middleware and how it's applied.
6. Flag any existing token-generation utilities already in the
   codebase (e.g., for password reset) that we could reuse instead
   of writing new logic.

Do not propose any implementation yet — I only need an accurate
summary of what already exists.

Prompt này quan trọng ở điểm cuối: yêu cầu rõ "không đề xuất implementation" — mục tiêu bước này thuần túy là thu thập sự thật, chưa phải thiết kế giải pháp. Nếu để agent nhảy thẳng vào đề xuất code, bạn dễ bị cuốn theo hướng của nó thay vì tự tư duy vấn đề trước.

Step 2: Viết Nháp Problem Statement Và Context

Từ thông tin thu thập được ở Step 1, bạn viết phần mở đầu của spec. Lưu ý: đây vẫn là bản nháp — sẽ được tinh chỉnh lại ở các bước sau khi bạn biết rõ hơn về edge case và ràng buộc.

## Context

The application currently supports email + password authentication
only. Auth routes live in `src/routes/auth.ts`. Sessions are stored
server-side in Redis with a 7-day TTL, referenced via an httpOnly,
secure, sameSite=lax cookie named `sid`. The `User` model
(src/prisma/schema.prisma) has `id`, `email`, `passwordHash`,
`createdAt`. Outbound email is sent via `src/services/emailService.ts`,
which wraps AWS SES and supports Handlebars templates with automatic
retry (3 attempts, exponential backoff) on transient SES errors.
No password-reset or token-based flow exists yet in this codebase,
so there is no existing token utility to reuse — this will be new.

## Problem Statement

A meaningful share of signup abandonment and support tickets trace
back to password friction: forgotten passwords, weak-password
rejections, and password-reset flows that add extra steps. Magic
link authentication removes the password entirely for the affected
user segment: the user enters their email, receives a single-use
sign-in link, and clicking it authenticates them directly.

This spec covers passwordless sign-in via email link for existing
and new users. It does NOT cover: social login (Google/GitHub OAuth),
removing password auth entirely (both will coexist), or SMS/OTP-based
login. Those are explicitly out of scope for this iteration.

Chú ý cách problem statement nêu rõ cả điều không làm (out of scope). Đây là chi tiết nhỏ nhưng cực kỳ quan trọng với agent — nếu không giới hạn scope, agent có xu hướng "làm luôn thể" các tính năng liên quan, dẫn đến phạm vi thay đổi vượt khỏi kiểm soát.

Step 3: Viết Functional Requirements

Đây là phần liệt kê cụ thể hệ thống phải làm gì, theo trình tự luồng nghiệp vụ:

## Functional Requirements

1. User submits their email address via `POST /auth/magic-link/request`.
2. System checks if a user with that email exists.
   - If exists: generate a single-use magic link token.
   - If not exists: respond with the same generic success message
     (do not reveal whether the email is registered — see security
     requirements).
3. Token generation:
   - Token is a cryptographically random 32-byte value, base64url-encoded.
   - Token is hashed (SHA-256) before storage; only the hash is
     persisted in the database, never the raw token.
   - Token expires 15 minutes after creation.
   - Token is single-use: once successfully verified, it is marked
     consumed and cannot be used again.
4. System emails the raw (unhashed) token embedded in a sign-in URL,
   e.g., `https://app.example.com/auth/magic-link/verify?token=<raw_token>`.
5. User clicks the link, which hits `GET /auth/magic-link/verify?token=...`.
6. System looks up the token by its hash, validates it is unexpired
   and unconsumed, marks it consumed, and creates a new session for
   the associated user (reusing the existing Redis session mechanism
   and `sid` cookie).
7. On successful verification, user is redirected to the app's
   authenticated landing page.
8. Rate limiting: no more than 3 magic-link requests per email
   address per 15-minute window, and no more than 10 requests per
   IP address per 15-minute window.

Ở bước này, đừng cố liệt kê mọi edge case ngay — mục tiêu là phác thảo "happy path" và các quy tắc cốt lõi trước. Edge case sẽ đến ở bước tiếp theo, có chủ đích.

Step 4: Dùng AI Để Tìm Ra Các Edge Case Bạn Bỏ Sót

Đây là nơi AI thể hiện giá trị rõ rệt nhất trong quá trình viết spec: nó đã "thấy" hàng loạt hệ thống auth tương tự và có thể liệt kê nhanh các edge case theo pattern, kể cả những case bạn không nghĩ tới ngay. Prompt hiệu quả cần cho AI xem functional requirements đã viết, và yêu cầu nó phản biện thay vì chỉ đồng tình:

Here are the draft functional requirements for a magic link
authentication feature (paste Step 3 content above). Act as a
skeptical senior engineer doing a design review. List every edge
case, race condition, or failure mode this spec does not currently
address. For each one, briefly state the scenario and why it
matters. Do not suggest solutions yet — just surface the gaps.

Consider especially: token lifecycle, concurrent requests, email
delivery failures, and cross-device usage.

Output mẫu điển hình mà AI có thể trả về:

- Expired token used: user clicks link after 15 minutes — what
  response/UX should occur?
- Reused token: user clicks the same link twice (e.g., email
  client pre-fetches the link for preview) — second click must fail
  gracefully, not silently succeed or silently fail with no message.
- Non-existent email requested: must not leak whether the email
  exists (timing side-channel too — hashing/lookup should take
  similar time whether user exists or not).
- Concurrent link requests: user requests a new link before the
  old one expires — should the old token be invalidated, or can
  both be valid simultaneously?
- Link opened on a different device/browser than the one that
  requested it: is this allowed? (Common case: user requests from
  phone, opens email on laptop.) If allowed, session should still
  be created correctly on the verifying device.
- Email delivery failure: SES call fails or bounces — does the
  request endpoint still return success, and how does the user find
  out the email never arrived?
- Malformed or tampered token in the verify URL: must fail safely
  without leaking whether a partial match exists.
- User account deleted/deactivated between requesting and clicking
  the link.
- Token enumeration/brute force: could an attacker guess a valid
  token via repeated verify attempts? What throttles this?
- Browser link-scanning/prefetching (corporate email security
  scanners) auto-clicking the link before the real user does,
  burning the single use.

Danh sách này rất giá trị — đặc biệt điểm cuối về email security scanner tự động click link là một edge case thực tế, khá tinh vi, dễ bị bỏ sót nếu chỉ tự brainstorm một mình.

Step 5: Viết Phần Edge Cases Và Error Handling

Từ danh sách Step 4, bạn quyết định xử lý cụ thể cho từng case và đưa vào spec chính thức:

## Edge Cases and Error Handling

- **Expired token**: Verify endpoint returns 400 with a user-facing
  message "This link has expired. Request a new one." Frontend
  shows a button to request a new link pre-filled with no email
  (user re-enters it) to avoid leaking prior email in the UI state.
- **Reused (already-consumed) token**: Verify endpoint returns 400
  with "This link has already been used." Treated identically to
  expired token in terms of response shape, to avoid distinguishing
  attack surface between the two.
- **Non-existent email on request**: Endpoint always returns 200
  with the generic message "If an account exists for this email, a
  sign-in link has been sent." Response time is normalized (see
  Non-Functional Requirements) to prevent timing-based enumeration.
- **New request while a prior token is still valid**: The prior
  unconsumed token is invalidated (deleted or marked consumed)
  when a new one is issued for the same user. Only the most recent
  link is ever valid.
- **Link opened on a different device**: Allowed by design. The
  verify endpoint has no device/session binding to the request step
  — only the token itself is checked. This is called out explicitly
  because it's a deliberate product decision, not an oversight.
- **Email delivery failure**: The request endpoint still returns
  the generic success response (to avoid enumeration), but the
  failure is logged with severity WARN including a correlation ID.
  A background alert fires if failure rate for magic-link emails
  exceeds 5% over 10 minutes.
- **Malformed token in verify URL**: Treated identically to an
  expired/invalid token — return the same generic 400, never a
  stack trace or DB error detail.
- **Account deactivated between request and click**: Verify
  endpoint checks account status at verification time; if inactive,
  return 403 with "This account is not active," and do not create
  a session.
- **Brute-force / token enumeration**: Verify endpoint is rate
  limited to 10 attempts per IP per 15 minutes, independent of the
  request-endpoint rate limit. Failed attempts increment a counter
  keyed by IP.
- **Email security scanners pre-clicking links**: Documented as a
  known limitation for v1. Mitigation deferred: if this becomes a
  measurable problem, consider a confirmation-click interstitial
  page instead of a GET that auto-verifies. Out of scope for this
  spec but noted for future iteration.

Chú ý cách mỗi mục không chỉ mô tả vấn đề mà còn quyết định rõ hành vi hệ thống — đây chính là điểm khác biệt giữa "liệt kê rủi ro" và "spec". Một agent đọc phần này biết chính xác phải code gì, không cần đoán.

Step 6: Viết Acceptance Criteria

Acceptance criteria viết theo dạng Given/When/Then, bao phủ các luồng chính và một số luồng lỗi quan trọng nhất — không cần cover 100% edge case ở đây (edge case đã có ở trên), chỉ cần đủ để xác nhận hệ thống hoạt động đúng theo happy path và các case rủi ro cao nhất.

## Acceptance Criteria

Scenario: Existing user requests a magic link
  Given a user with email "jane@example.com" exists in the system
  When they POST to /auth/magic-link/request with that email
  Then the response is 200 with the generic success message
  And exactly one unconsumed token is created for that user
  And an email containing a valid magic-link URL is sent to
    jane@example.com within 5 seconds

Scenario: Non-existent email requests a magic link
  Given no user exists with email "ghost@example.com"
  When a POST is made to /auth/magic-link/request with that email
  Then the response is 200 with the same generic success message
    as the existing-user case
  And no token is created
  And no email is sent

Scenario: User successfully verifies a valid magic link
  Given a valid, unexpired, unconsumed token exists for a user
  When they GET /auth/magic-link/verify?token=<raw_token>
  Then a new session is created for that user
  And the sid cookie is set with httpOnly, secure, sameSite=lax
  And the token is marked consumed
  And the user is redirected to the authenticated landing page

Scenario: User attempts to reuse a consumed token
  Given a token that has already been successfully verified once
  When the same token is used again via GET /auth/magic-link/verify
  Then the response is 400 with "This link has already been used."
  And no new session is created

Scenario: User attempts to verify an expired token
  Given a token was issued 16 minutes ago (TTL is 15 minutes)
  When it is used via GET /auth/magic-link/verify
  Then the response is 400 with "This link has expired."
  And no session is created

Scenario: Rate limit is enforced on repeated requests
  Given a user has requested 3 magic links for the same email
    within the last 15 minutes
  When a 4th request is made for the same email within that window
  Then the response is 429 with a rate-limit message
  And no new token is created and no email is sent

Step 7: Nhờ AI Review Lại Spec

Trước khi giao spec cho agent implement, hãy để AI đóng vai reviewer một lần nữa — lần này review toàn bộ spec đã gần hoàn chỉnh, tìm mâu thuẫn nội bộ, lỗ hổng bảo mật, hoặc requirement chưa nhất quán với nhau.

Review this complete spec draft for magic link authentication
(paste the full spec so far: Context, Problem Statement, Functional
Requirements, Edge Cases and Error Handling, Acceptance Criteria).

Act as a staff engineer doing a pre-implementation design review.
Specifically check for:
1. Contradictions between sections (e.g., does an acceptance
   criterion conflict with an edge case rule?).
2. Security gaps: anything related to token storage, timing attacks,
   enumeration, or session fixation that isn't addressed.
3. Requirements that are ambiguous enough that two engineers could
   implement them differently.
4. Anything in Functional Requirements that has no corresponding
   Acceptance Criteria, or vice versa.

List concrete issues only. Do not rewrite the spec — just point
out problems.

Phản hồi hữu ích điển hình mà AI có thể chỉ ra ở vòng review này: "Functional Requirement 3 nói token được hash bằng SHA-256 trước khi lưu, nhưng không có acceptance criterion nào kiểm tra rằng raw token không xuất hiện trong DB hoặc log — nên thêm một tiêu chí kiểm tra việc này." Hoặc: "Edge case về 'new request invalidates old token' không có acceptance criterion tương ứng — nên thêm scenario kiểm chứng." Đây chính xác là loại phản hồi có giá trị: cụ thể, có thể hành động, chỉ ra khoảng trống thật sự thay vì khen chung chung.

Step 8: Bổ Sung Non-Functional Requirements

Sau khi đã xử lý phản hồi review, bổ sung phần yêu cầu phi chức năng — phần này thường quyết định spec có "production-grade" hay không:

## Non-Functional Requirements

- **Token entropy**: Raw token must be generated using a
  cryptographically secure random source (Node's `crypto.randomBytes`),
  minimum 32 bytes (256 bits) before encoding.
- **Token storage**: Only the SHA-256 hash of the token is persisted.
  Raw token must never appear in application logs, error messages,
  or database records at any point after generation.
- **Token expiry**: Fixed at 15 minutes, not configurable per-request
  by the client.
- **Timing consistency**: The /request endpoint must take
  statistically similar time to respond whether the email exists or
  not, to prevent user-enumeration via timing side-channel (achieved
  by always performing an equivalent-cost hash/lookup operation on
  both paths).
- **Rate limiting**: As specified in Functional Requirements (3 per
  email / 15 min, 10 per IP / 15 min for request; 10 per IP / 15 min
  for verify attempts). Backed by the existing Redis instance, not
  in-memory, so limits hold across multiple app instances.
- **Email delivery SLA**: Magic-link email must be handed off to the
  SES send call within 2 seconds of request acceptance (P95). Actual
  inbox delivery is outside this system's control but delivery
  failures must be observable (see logging requirement below).
- **Session consistency**: Sessions created via magic link must be
  indistinguishable in structure from password-login sessions,
  reusing the same Redis schema and cookie configuration.
- **Observability**: Every magic-link request, verification success,
  verification failure (with reason: expired/consumed/invalid/rate-
  limited), and email delivery failure must emit a structured log
  event with a correlation ID, without ever including the raw token
  value.
- **Data retention**: Consumed and expired tokens are retained for
  30 days for audit/abuse-investigation purposes, then purged by a
  scheduled job, never indefinitely.

Step 9: Kiểm Chứng Spec Có Tạo Ra Output Chất Lượng Cao Từ Agent Không

Bước cuối cùng trước khi giao toàn bộ spec cho agent implement full: chạy thử một mảnh nhỏ của spec trước, xem output có đúng như kỳ vọng không. Đừng giao cả spec 6 phần cho agent chạy một lèo rồi mới phát hiện vấn đề ở review cuối — điều đó tốn kém hơn nhiều so với việc phát hiện sớm.

Cách làm: chọn phần rủi ro cao nhất hoặc mơ hồ nhất trong spec (ở đây là logic sinh và verify token), giao cho agent implement riêng phần đó kèm chỉ mô tả liên quan, rồi đánh giá kết quả trước khi tiến hành phần còn lại.

Using only the token-generation and token-verification logic
described in the Functional Requirements, Edge Cases, and
Non-Functional Requirements sections below (paste those three
sections only), implement just the token utility module:
- a function to generate a token (raw + hash pair)
- a function to verify a raw token against a stored hash, checking
  expiry and consumed status

Do not implement the routes or email sending yet. After writing the
code, list which requirements from the spec you were unsure about
or had to make an assumption on.

Điểm mấu chốt ở đây là câu cuối: yêu cầu agent tự báo cáo những chỗ nó phải "đoán". Nếu agent liệt kê ra bất kỳ điểm mơ hồ nào, đó chính là tín hiệu cho bạn quay lại sửa spec trước khi giao toàn bộ tính năng — rẻ hơn rất nhiều so với phát hiện sau khi cả feature đã được implement sai hướng.

Mẹo: Luôn thử "trial run" trên phần rủi ro cao nhất của spec trước, không phải phần dễ nhất. Test thử phần dễ sẽ luôn ra kết quả tốt và cho bạn cảm giác an toàn giả — trong khi phần khó (ở đây là token lifecycle) mới là nơi spec của bạn thực sự bị thử thách.

Ví Dụ Spec Hoàn Chỉnh

Dưới đây là bản spec hoàn chỉnh, tổng hợp lại toàn bộ các phần đã xây dựng qua các bước thực hành ở trên, trình bày như một tài liệu sẵn sàng giao cho agent implement. Hãy chú ý ba điều khi đọc: (1) mỗi phần đều tự đứng vững và không phụ thuộc vào việc phải hỏi lại tác giả, (2) các quyết định mơ hồ tiềm ẩn (ví dụ: link mở trên thiết bị khác, email không tồn tại) đều đã được quyết định rõ ràng chứ không để ngỏ, và (3) acceptance criteria bao phủ đúng những rủi ro cao nhất đã nêu ở edge case, tạo thành một vòng khép kín từ vấn đề đến kiểm chứng.

Context

The application currently supports email + password authentication
only. Auth routes live in `src/routes/auth.ts`. Sessions are stored
server-side in Redis with a 7-day TTL, referenced via an httpOnly,
secure, sameSite=lax cookie named `sid`. The `User` model
(src/prisma/schema.prisma) has `id`, `email`, `passwordHash`,
`createdAt`. Outbound email is sent via `src/services/emailService.ts`,
which wraps AWS SES and supports Handlebars templates with automatic
retry (3 attempts, exponential backoff) on transient SES errors.
No password-reset or token-based flow exists yet in this codebase,
so there is no existing token utility to reuse — this will be new.

Problem Statement

A meaningful share of signup abandonment and support tickets trace
back to password friction: forgotten passwords, weak-password
rejections, and password-reset flows that add extra steps. Magic
link authentication removes the password entirely for the affected
user segment: the user enters their email, receives a single-use
sign-in link, and clicking it authenticates them directly.

This spec covers passwordless sign-in via email link for existing
and new users. It does NOT cover: social login (Google/GitHub OAuth),
removing password auth entirely (both will coexist), or SMS/OTP-based
login. Those are explicitly out of scope for this iteration.

Functional Requirements

1. User submits their email address via POST /auth/magic-link/request.
2. System checks if a user with that email exists.
   - If exists: generate a single-use magic link token.
   - If not exists: respond with the same generic success message
     (do not reveal whether the email is registered).
3. Token generation:
   - Cryptographically random 32-byte value, base64url-encoded.
   - Hashed (SHA-256) before storage; only the hash is persisted.
   - Expires 15 minutes after creation.
   - Single-use: consumed on successful verification, cannot be
     reused.
4. System emails the raw token embedded in a sign-in URL, e.g.,
   https://app.example.com/auth/magic-link/verify?token=<raw_token>.
5. User clicks the link, hitting GET /auth/magic-link/verify?token=...
6. System looks up the token by hash, validates unexpired and
   unconsumed, marks consumed, creates a new session (reusing
   existing Redis session mechanism and sid cookie).
7. On success, user is redirected to the authenticated landing page.
8. Rate limiting: max 3 requests per email per 15-minute window,
   max 10 requests per IP per 15-minute window.
9. Issuing a new token for a user invalidates any prior unconsumed
   token for that same user.

Non-Functional Requirements

- Token entropy: crypto.randomBytes, minimum 32 bytes before encoding.
- Token storage: only SHA-256 hash persisted; raw token never logged
  or stored.
- Token expiry fixed at 15 minutes, not client-configurable.
- Timing consistency: /request endpoint response time must not
  reveal whether the email exists.
- Rate limiting backed by shared Redis, not in-memory, to hold
  across multiple app instances.
- Email handoff to SES within 2 seconds (P95) of request acceptance.
- Sessions from magic link are structurally identical to
  password-login sessions.
- All request/verify/failure events logged with correlation ID,
  never including raw token value.
- Consumed/expired tokens retained 30 days for audit, then purged
  by scheduled job.

Edge Cases and Error Handling

- Expired token: 400, "This link has expired. Request a new one."
- Reused/consumed token: 400, "This link has already been used."
- Non-existent email: 200 with generic success message, no token
  created, no email sent, response time normalized.
- New request while prior token valid: prior token invalidated,
  only most recent link is valid.
- Link opened on a different device: allowed by design, no device
  binding enforced.
- Email delivery failure: request endpoint still returns generic
  success; failure logged at WARN with correlation ID; alert fires
  if failure rate > 5% over 10 minutes.
- Malformed token: treated identically to expired/invalid, generic
  400, no internal error detail leaked.
- Account deactivated between request and click: 403, "This account
  is not active," no session created.
- Brute-force on verify endpoint: rate limited to 10 attempts per IP
  per 15 minutes, tracked independently of the request-endpoint limit.
- Email security scanners pre-clicking links: known v1 limitation,
  documented, mitigation deferred to a future iteration.

Acceptance Criteria

Scenario: Existing user requests a magic link
  Given a user with email "jane@example.com" exists
  When they POST to /auth/magic-link/request with that email
  Then the response is 200 with the generic success message
  And exactly one unconsumed token is created for that user
  And a valid magic-link email is sent within 5 seconds

Scenario: Non-existent email requests a magic link
  Given no user exists with email "ghost@example.com"
  When a POST is made to /auth/magic-link/request with that email
  Then the response is 200 with the same generic success message
  And no token is created and no email is sent

Scenario: User successfully verifies a valid magic link
  Given a valid, unexpired, unconsumed token exists
  When they GET /auth/magic-link/verify?token=<raw_token>
  Then a new session is created and the sid cookie is set
    (httpOnly, secure, sameSite=lax)
  And the token is marked consumed
  And the user is redirected to the authenticated landing page

Scenario: User attempts to reuse a consumed token
  Given a token already successfully verified once
  When the same token is used again
  Then the response is 400 with "This link has already been used."
  And no new session is created

Scenario: User attempts to verify an expired token
  Given a token issued 16 minutes ago (TTL is 15 minutes)
  When it is used to verify
  Then the response is 400 with "This link has expired."
  And no session is created

Scenario: Rate limit is enforced on repeated requests
  Given 3 magic-link requests already made for the same email
    within the last 15 minutes
  When a 4th request is made for the same email in that window
  Then the response is 429 with a rate-limit message
  And no new token is created and no email is sent

Mẹo: Khi giao spec hoàn chỉnh này cho agent, hãy giao theo từng phần nhỏ như đã làm ở Step 9 nếu tính năng có nhiều rủi ro (auth luôn thuộc nhóm này), thay vì giao cả sáu phần và yêu cầu implement toàn bộ trong một lần. Chi phí chia nhỏ nhỏ hơn nhiều so với chi phí phải review lại một PR lớn có lỗi bảo mật ẩn trong logic token.

Những điểm chính

  • Spec là artifact có đòn bẩy cao nhất khi làm việc với AI agent: đầu tư 30-45 phút viết spec kỹ thường tiết kiệm nhiều giờ rework, review, và debug production sau này.
  • Một spec production-grade cần đủ sáu phần: Context, Problem Statement, Functional Requirements, Non-Functional Requirements, Edge Cases and Error Handling, và Acceptance Criteria — thiếu phần nào, agent sẽ phải tự đoán phần đó.
  • Thu thập context từ codebase thật trước khi viết spec — đừng để agent (hay chính bạn) tự suy diễn kiến trúc hiện có.
  • Dùng AI như một brainstorm partner để tìm edge case bạn dễ bỏ sót (token reuse, race condition, timing side-channel), nhưng luôn tự quyết định cách xử lý từng case, đừng để AI quyết định thay.
  • Làm một vòng spec review với AI trước khi implement — tìm mâu thuẫn nội bộ và khoảng trống giữa functional requirements, edge case, và acceptance criteria.
  • Luôn giới hạn scope rõ ràng (nêu cả điều "không làm") để tránh agent tự mở rộng phạm vi ngoài ý muốn.
  • Trước khi giao toàn bộ spec để implement, thử nghiệm trên một mảnh nhỏ rủi ro cao nhất và yêu cầu agent tự báo cáo điểm mơ hồ nó gặp phải — đó là tín hiệu sớm rẻ nhất để phát hiện spec còn thiếu sót.