Tài liệu thực chiến

Dựng hub nội bộ có kênh chat cho công ty bằng Claude Code

Ba lớp, bảy bước, và mười cái bẫy đã trả giá bằng tiền thật. Viết cho người không biết code.

Đúc từ hệ IVY HUB của The Ivy Haus — đang chạy thật cho khoảng 17 nhân viên. Login sống 30/5/2026 · phòng chat chung 5/6/2026 · chat nhiều phòng 8/6/2026. Mọi cái bẫy trong tài liệu này là bẫy đã dính thật, không phải lý thuyết.

PHẦN 0

Hiểu bản chất trước khi bấm gì

Một cái "hub công ty có kênh chat" thực chất chỉ gồm ba lớp, không hơn.

Bộ não
Database + đăng nhập + phân quyền + realtime + kho file
Supabase
Free → Pro khi đông người
Mặt tiền
Trang web nhân viên mở lên hằng ngày
Cloudflare Pages
Free thật, không giới hạn bandwidth
Người thợ
Viết toàn bộ code, chạy lệnh, gọi API
Claude Code
Theo gói Claude

Điều quan trọng nhất phải hiểu: Claude Code không phải "app tạo hub". Nó là một lập trình viên ngồi trong máy bạn — đọc và ghi file, chạy lệnh, gọi API. Chất lượng hub phụ thuộc vào việc bạn mô tả công ty mình rõ tới đâu, không phụ thuộc vào việc bạn biết code hay không.

Đừng nghĩ tới việc tự viết app chat từ đầu (WebSocket, server riêng…). Supabase đã có sẵn Realtime + Auth + Storage + phân quyền theo từng dòng dữ liệu (RLS). Việc còn lại chỉ là khoảng 5 bảng SQL và 1 file HTML. Nhà tôi chạy nguyên cái hub bằng HTML tĩnh + Supabase, không có server backend nào cả.

PHẦN 1

Cần chuẩn bị những gì

Tài khoản cần đăng ký — làm trước, khoảng 30 phút

  1. Claude (để dùng Claude Code) — claude.ai
  2. Supabase — supabase.com → tạo 1 project.
    • Region: chọn gần Việt Nam (Singapore hoặc Tokyo). Chênh lệch tốc độ giữa hai cái này không đáng kể.
    • Đặt mật khẩu database và lưu lại ngay — không xem lại được.
  3. Cloudflare — dash.cloudflare.com → phần Workers & Pages.
  4. Tên miền (không bắt buộc lúc đầu): công ty đã có congty.vn thì trỏ thêm subdomain hub.congty.vn. Chưa có thì dùng tạm link miễn phí <tên-project>.pages.dev.

Cài trên máy

Claude Code sẽ hướng dẫn nếu vướng — cần Node.js (bản LTS) và Git, rồi:

npm install -g @anthropic-ai/claude-code

Ba chìa khoá phải lấy — đây là chỗ đa số người mới tắc

Chìa khoáLấy ở đâuDùng để làm gìMức bí mật
Project URL + anon key Supabase → Project Settings → API Nhúng thẳng vào file HTML cho trình duyệt gọi An toàn để lộ (đã có RLS chặn)
Supabase Access Token (Management API) Supabase → Account → Access Tokens Để Claude tự chạy SQL thay bạn, khỏi copy-paste tay Tuyệt mật
Cloudflare API Token Cloudflare → My Profile → API Tokens, quyền Account > Cloudflare Pages: Edit Để Claude tự deploy web lên Tuyệt mật

Luật sống còn. Supabase còn một key nữa tên service_role / secret. KHÔNG BAO GIỜ nhúng key này vào file HTML hay đưa lên web — nó bỏ qua mọi phân quyền, ai cầm được là đọc sạch database. Chỉ dùng trong script chạy trên máy.

Không dán token vào khung chat. Tạo file secrets/.env trong thư mục dự án, viết token vào đó, và bảo Claude thêm dòng secrets/ vào .gitignore ngay từ đầu.

PHẦN 2

Bảy bước thao tác

Các prompt in nghiêng bên dưới copy-paste được thẳng vào Claude Code.

BƯỚC 1

Tạo thư mục dự án và "dạy" Claude về công ty

Bước này quyết định 80% chất lượng hub.

mkdir C:\CongTy_Hub
cd C:\CongTy_Hub
claude

Việc đầu tiên không phải là bảo nó code, mà là tạo file CLAUDE.md — file này Claude tự đọc mỗi lần mở phiên mới.

Prompt

"Tạo cho tôi file CLAUDE.md mô tả dự án. Công ty tôi tên [X], làm [ngành gì], có [N] nhân viên chia thành các bộ phận: [liệt kê]. Tôi muốn dựng một hub nội bộ dùng Supabase + Cloudflare Pages. Quy tắc: luôn trả lời tôi bằng tiếng Việt; không được bịa số liệu; trước khi sửa gì phải đọc file thật."

Càng tả kỹ ai làm gì, phân quyền càng đúng ngay từ đầu, đỡ đập đi xây lại.

BƯỚC 2

Dựng nền: đăng nhập + vai trò + phân quyền

Prompt

"Viết cho tôi file SQL 01_schema.sql idempotent (chạy lại nhiều lần không hỏng data) gồm:

  • bảng roles: các vai trò [owner, giám đốc, sale, kế toán, kho, marketing]
  • bảng profiles gắn 1-1 với auth.users, có trigger tự tạo profile khi có người đăng ký mới
  • hàm is_admin() kiểu SECURITY DEFINER để tránh RLS đệ quy
  • bật RLS: nhân viên chỉ đọc hồ sơ của mình, owner đọc tất cả"

Rồi: "Đây là Supabase Access Token của tôi trong secrets/.env. Viết script chạy file SQL này lên Supabase qua Management API, rồi chạy giúp tôi."

Xong bước này bạn đã có: hệ đăng nhập + phân quyền ở cấp database. Kể cả người ngoài lấy được key nhúng trong web cũng không đọc trộm được dữ liệu.

BƯỚC 3

Trang web đăng nhập — một file HTML duy nhất

Prompt

"Viết file HUB.html — một trang tự chạy, không cần build, dùng @supabase/supabase-js@2 từ CDN. Có màn đăng nhập bằng email + mật khẩu (signInWithPassword), có tuỳ chọn ghi nhớ đăng nhập. Sau khi login thì đọc profiles.role_key và chỉ hiện những khu tương ứng với vai trò đó."

Kinh nghiệm nhà tôi: cả hub — hơn 40 module — vẫn chỉ là 1 file HTML chính + mỗi module 1 file .js nạp khi cần. Đừng dựng React/Next làm gì cho nặng nếu đội không có lập trình viên nuôi nó.

BƯỚC 4

Đưa lên mạng (Cloudflare Pages)

Nếu máy có Node, đây là con đường ngắn nhất:

npx wrangler pages deploy ./web --project-name=congty-hub

Sau đó vào Cloudflare → project → Custom domains → thêm hub.congty.vn. HTTPS tự cấp, không phải mua.

Nhà tôi phải tự viết script gọi API deploy vì máy không cài được Node lúc đó — và trả giá kha khá (xem bẫy 02). Nếu bạn cài được wrangler thì đừng đi đường vòng đó.

Vì sao là Cloudflare mà không phải Netlify: nhà tôi khởi đầu bằng Netlify, đến 1/6/2026 thì Netlify đổi gói Free sang mô hình credits — deploy vài lần là hết credit, cả tài khoản bị pause, mọi site trả lỗi 503, phải chờ 16 ngày mới reset. Buộc phải chuyển gấp sang Cloudflare Pages ngay trong đêm. Cloudflare Pages Free hiện không giới hạn bandwidth/request.

BƯỚC 5 · PHẦN CHÍNH

Kênh chat

Đây là kiến trúc thật đang chạy ở nhà tôi. Đưa nguyên khối này cho Claude Code:

Prompt — schema chat

"Dựng module chat nội bộ trên Supabase theo thiết kế sau, viết SQL idempotent:

Bảng hub_chat_messages: id (bigint identity), user_id (FK auth.users), sender_name (lưu sẵn tên lúc gửi), role_key (để hiện badge chức danh), message (text, chặn tin rỗng), room_id (bigint, NULL = phòng chung ai cũng đọc), reply_to (tự tham chiếu — để trả lời tin), mentions (text[]), is_pinned/pinned_by/pinned_at (ghim tin), edited_at, deleted_at/deleted_by (xoá mềm, không xoá cứng), created_at. Index theo (room_id, created_at)(created_at desc).

Bảng hub_chat_rooms: id, name, created_by, created_by_name, created_at.
Bảng hub_chat_room_members: (room_id, user_id) làm khoá chính, added_by, added_at.
Bảng hub_chat_reads: user_id (khoá chính), last_read_id, last_read_at — để hiện badge "N tin chưa đọc".

Hàm helper hub_chat_is_member(room_id) kiểu SECURITY DEFINER, trả về true nếu auth.uid() có trong bảng members.

RLS (quan trọng nhất):

  • Đọc tin: room_id is null OR hub_chat_is_member(room_id) OR is_admin()
  • Gửi tin: auth.uid() = user_id AND (room_id is null OR hub_chat_is_member(room_id))
  • Bảng hub_chat_reads: mỗi người chỉ đụng được dòng của chính mình.

RPC cần có: hub_chat_room_create(tên, mảng_uuid_thành_viên) · hub_chat_rooms_mine() (kèm số thành viên + thời điểm tin cuối, sắp xếp phòng mới nhất lên đầu) · hub_chat_room_add(room, mảng_uuid) (chỉ người tạo phòng hoặc admin) · hub_chat_mark_read(msg_id) · hub_chat_unread_count().

Realtime: thêm 3 bảng hub_chat_messages, hub_chat_rooms, hub_chat_room_members vào publication supabase_realtime (bọc trong khối do $$ … $$ kiểm tra tồn tại trước để chạy lại được)."

Prompt — giao diện

"Viết module chat trong hub: cột trái là danh sách phòng (gọi hub_chat_rooms_mine()), cột phải là khung tin nhắn. Đăng ký kênh Supabase Realtime để tin mới hiện ngay không cần F5. Có nút tạo nhóm mới chọn thành viên từ danh sách nhân viên, trả lời tin, ghim tin, và badge số tin chưa đọc từng phòng."

Gửi ảnh / file: tạo bucket Storage tên chat-uploads (để public), thêm cột attachments kiểu jsonb vào bảng tin nhắn, mỗi file lưu {url, name, path, size, type}.

BƯỚC 6

Tạo tài khoản cho nhân viên

Đây là chỗ thực tế đá vào lý thuyết: nhiều nhân viên không có email, hoặc có mà không nhớ mật khẩu.

Cách nhà tôi làm và chạy tốt: cho đăng nhập bằng số điện thoại. Frontend tự ghép số thành email ảo <số điện thoại>@congty.local, mật khẩu chung ban đầu ví dụ congty2026. Nhân viên chỉ cần nhớ số của chính mình.

Prompt

"Sửa form đăng nhập: nếu người dùng gõ 10 chữ số thì tự ghép thành <số>@congty.local trước khi gọi signInWithPassword. Viết SQL tạo sẵn N tài khoản theo danh sách nhân viên kèm vai trò."

Đánh đổi: email ảo thì không tự reset mật khẩu qua email được — admin phải reset hộ bằng một câu SQL. Chấp nhận được, nhưng phải biết trước.

BƯỚC 7

Bàn giao cho nhân viên

  • Viết một trang hướng dẫn đăng nhập kèm ảnh chụp màn hình, gửi qua Zalo.
  • Bảo Claude thêm manifest.json + service worker → nhân viên "Thêm vào màn hình chính" trên điện thoại, dùng như app thật.
  • Tuần đầu ngồi cạnh xem họ bấm gì — 90% góp ý hữu ích đến từ đây, không đến từ bản thiết kế.
PHẦN 3

Mười cái bẫy đã trả giá

Phần đáng tiền nhất tài liệu này.

  1. Netlify Free đổi sang mô hình credits

    Deploy vài lần là khoá cả tài khoản, mọi site 503, chờ 16 ngày reset. → Dùng Cloudflare Pages. Và luật chung: trước khi chọn nhà cung cấp nào, đọc chính sách gói Free HIỆN TẠI, đừng tin bài viết cũ.

  2. Đừng tự viết script deploy nếu không bắt buộc

    Cloudflare Pages Direct Upload đòi hash BLAKE3 (không phải SHA256), đòi lấy upload-token riêng, đòi gửi kèm manifest. Sai một bước là lỗi 400/401 mà thông báo lỗi không nói rõ. wrangler làm hết trong một lệnh.

  3. Deploy xong không thấy đổi gì → là do cache của service worker

    Mỗi lần deploy phải tăng số VERSION trong sw.js, và bảo nhân viên Ctrl+Shift+R. Không biết luật này sẽ ngồi sửa code đúng mà tưởng mình sai.

  4. RLS đệ quy vô hạn

    Viết policy kiểu "được đọc nếu là admin", mà điều kiện admin lại query chính bảng đó → Postgres treo. Cách chữa: đưa phép kiểm tra vào hàm SECURITY DEFINER (is_admin(), hub_chat_is_member()), policy chỉ gọi hàm.

  5. Mọi câu INSERT dữ liệu mẫu phải idempotent

    insert … where not exists. Nhà tôi chạy lại file SQL một lần → danh sách công việc nhân đôi.

  6. Tiếng Việt mất dấu — bẫy nhiều tầng, mất nhiều giờ nhất
    • Font Georgia không đủ tiếng Việt → chữ "ầ/ề/ễ" bị tách dấu. Dùng Lora hoặc Be Vietnam Pro (có subset vietnamese), và bỏ Georgia khỏi cả danh sách fallback.
    • Nếu chạy script bằng Windows PowerShell 5.1: nó gửi nội dung theo Latin1 → tiếng Việt biến thành ? ngay trên đường truyền. Phải đọc file bằng [IO.File]::ReadAllText($f,[Text.Encoding]::UTF8) và gửi bằng mảng byte UTF-8.
    • Console PowerShell in ra Bản không có nghĩa là data hỏng — nó chỉ đọc sai lúc hiển thị. Luôn kiểm tra dữ liệu thật trên giao diện Supabase, đừng tin cửa sổ console.
  7. Không bao giờ xoá cứng một tài khoản

    Bảng tin nhắn có khoá ngoại ON DELETE CASCADE → xoá một nhân viên nghỉ việc là bay sạch toàn bộ tin nhắn của người đó, mất luôn lịch sử công việc. Người nghỉ việc thì khoá tài khoản (banned_until = 'infinity') + đổi mật khẩu ngẫu nhiên + gỡ khỏi các phòng. Chat xoá tin cũng vậy: dùng deleted_at (xoá mềm), không DELETE.

  8. Đừng để lộ dữ liệu nhạy cảm theo kiểu "ẩn trên giao diện"

    Giá vốn, lương, VAT — nếu chỉ ẩn ở frontend thì mở DevTools là thấy. Phải chặn ở RLS cấp database: phòng chat lương/kế toán chỉ hiện với thành viên phòng.

  9. Coi chừng hoá đơn Supabase ở phần ảnh

    Bật tính năng biến đổi ảnh (image transform) mà không giới hạn → chi phí vọt. Nhà tôi phải bật Spend Cap và dựng cảnh báo theo dõi mức dùng. Bật Spend Cap ngay từ ngày đầu.

  10. Cronjob "chạy xanh" không có nghĩa là dữ liệu đã vào

    Nhà tôi có cữ tự động báo thành công 96 lần/ngày trong khi nguồn dữ liệu dính cache 12 tiếng → bảng hiển thị số cũ mà không ai biết. Mọi tác vụ tự động phải kiểm 2 lớp: lớp 1 "lệnh chạy xong chưa", lớp 2 "số trong bảng có mới không".

PHẦN 4

Chi phí & thời gian thực tế

Tiền

  • Cloudflare Pages:
  • Supabase: bắt đầu Free. Khi đông người dùng / dữ liệu lớn thì lên Pro — giá xem tại trang Supabase lúc đăng ký, có thay đổi theo thời điểm.
  • Claude: theo gói đang dùng.
  • Tên miền: công ty đã có sẵn thì thêm subdomain miễn phí.

Thời gian — mốc thật của nhà tôi

Cột mốcMất bao lâu
Trang login sống + phân quyền theo vai trò~1 ngày làm việc
Phòng chat chung cho cả công tythêm ~1 tuần sau đó
Chat nhiều phòng, thành viên riêng, gửi file/ảnhthêm ~3 ngày nữa
Từ đó đến naykhông bao giờ hết — mỗi bộ phận lại cần một module riêng

Cái dòng cuối là dấu hiệu tốt: nghĩa là người ta dùng thật.

PHẦN 5

Lời khuyên quan trọng nhất

Đừng dựng hub trước rồi mới hỏi nhân viên cần gì. Hãy làm ngược lại: hỏi từng bộ phận "một ngày của em kẹt ở chỗ nào nhất?" → dựng đúng chỗ đó trước. Nhà tôi có những module dựng xong đẹp đẽ mà 0 dòng dữ liệu suốt một tháng rưỡi vì trùng chức năng với chỗ khác — cuối cùng phải gỡ bỏ.

Kênh chat nên là thứ dựng đầu tiên sau màn đăng nhập. Nó là lý do khiến nhân viên mở hub mỗi ngày. Có thói quen mở rồi thì đặt module gì vào cũng có người dùng; chưa có thói quen thì module hay tới đâu cũng nằm im.

Và: bắt Claude Code kiểm chứng, đừng tin lời nó báo cáo. Câu nên nói thường xuyên nhất là:

"Đừng nói đã xong — hãy chạy thử rồi đưa tôi xem kết quả thật."