我今日開源咗自己個營銷平台嘅第一個版本。
佢叫 STRAŦUM(斯特拉圖姆)。九個 AI Agent,一個做到每個客戶數據分開嘅 agency workspace,十種語言。我喺 2025 年一邊自學寫 code,一邊砌佢出嚟。之後我開始做第二個版本,與其將第一個版本鎖埋喺度,我選擇咗將佢送出去。
如果你想睇下先再讀落去: github.com/chandlernguyen/stratum-oss
git clone https://github.com/chandlernguyen/stratum-oss
如果你係帶緊 marketing 團隊、而且永遠都唔會打開嗰個 repo 嘅人,可以直接跳到最尾嗰節「如果你根本唔會睇 code」。嗰節係寫畀你嘅,而且好短。
我想講清楚嗰個 repo 到底係咩,因為「開源」可以代表好多嘢,而當中大部分都誇大咗佢喺呢度嘅意思。
我實際上發布咗啲咩
STRAŦUM v1 已經發布,但唔再積極開發。啲 code 以 MIT 授權公開。冇 roadmap、冇 release schedule、冇任何支援承諾——README 係咁寫,contributing guide 再講多一次。
佢係以參考實作嘅形式發布。佢係一件可以攞嚟睇、攞嚟 fork,或者喺入面攞啲 parts 出嚟用嘅嘢。佢唔係一件可以依賴嘅嘢。冇人會跟住未來嘅 dependency 變化去維護佢,而個 repo 亦都老實列出佢知道嘅 security advisories,而唔係當佢哋唔存在。
我唔係話佢永遠都唔會變。我唔會再繼續開發佢,所以你應該當佢就係而家咁去規劃——但別人發畀我嘅嘢我係會睇嘅,如果有人報咗個 bug,或者建議咗啲令啲 code 更清楚嘅做法,我唔會扮冇睇到。
發布佢,亦都意味住要證明冇任何機密嘢跟埋一齊出去。個 repo 入面有個 script,會掃描每一個被 git 追蹤嘅檔案,搵 credentials、private keys、provider tokens 同 production identifiers,而佢係 CI 入面第一個 job——排喺啲 test 前面——因為一條洩漏咗嘅 key,喺 push 嗰一刻就已經入咗 history,事後刪返都抹唔走。
以下係佢入面有嘅嘢:
- 九個 Agent——策略、Persona、內容、成效情報、競爭情報、Campaign 規劃、客戶成功,仲有兩個。每一個都係一個 base agent 嘅 subclass,共用一套 prompt 結構、一個 tool registry,同埋 progressive context。
- 兩個 Postgres schema。 個別生意嘅數據放喺一個,agency 嘅數據放喺另一個,以 Row Level Security 做隔離邊界。
- 十個 locale,locale 會經 header 傳去 API,令生成嘅 metadata 同介面語言對得上。
- 一個唔需要 API key 嘅 demo mode。 Agent 唔會 call model,而係回傳清楚標明咗嘅罐頭輸出,所以你可以冇 provider account 都撳勻成個應用。
如果你想整一個自己嘅版本
呢個係我最想見到嘅用法,所以值得明講,而唔係留喺暗示度。
如果你真正想要嘅,係自己一套 marketing agent 系統——你自己嘅 Agent、你自己嘅 prompt、你自己嘅 schema、喺上面起你自己嘅產品——咁就 fork 個 repo,去整佢出嚟。MIT 授權就係為咗呢樣,佢係預期之內嘅用法,唔係咩漏洞。除咗保留個 licence file,冇任何署名要求,亦都唔需要問過我先。攞走有用嘅 parts,掉走冇用嘅,凡係你唔認同嘅地方就改。
比起得一個淨係我一個人掂過嘅版本,我更想見到十幾個唔同嘅版本。
點解要發布 v1,而唔係自己收埋
我擺咗佢喺度十個月冇出,因為我以為冇人會想要一個我已經停咗手嘅 v1。呢個假設係錯嘅,而佢錯喺邊,比個 repo 本身更有價值。
第二個版本行嘅係另一條路。我而家砌緊嘅嘢,同呢個 codebase 已經唔係好似,將來亦都唔會係佢嘅一個 diff。將 v1 收埋喺 private repo,只會令佢慢慢變成一件冇人入得去睇嘅博物館展品。
更好嘅理由係:一個真係跑得起嘅 multi-tenant agentic 應用,比一張架構圖有用得多。我自學嗰陣,幫到我嘅唔係嗰啲概念解釋——而係搵到一個真實項目,睇下別人實際上點樣安排佢。喺解釋入面唔見咗嘅嗰部分推理,先至係最重要嘅推理。
我喺個 repo 嘅架構筆記入面寫咗四樣我做錯咗、唔會再做嘅嘢,而四樣我都原封不動留咗喺度。第一樣係一個 security bug。
出咗街嘅嗰個錯誤
有一張表出街嗰陣,冇開 Row Level Security。
public.notification_push_deliveries 喺建立嗰陣少咗啟用 RLS 嗰行,仲畀咗 anonymous 同 authenticated 角色完整權限——包括 TRUNCATE——而且一條 policy 都冇定義。佢喺 Data API 暴露嘅嗰個 schema 入面,而 anonymous key 係包喺 browser bundle 入面一齊出街嘅。所以喺嗰段時間裏面,任何揸住呢條 key 嘅人都讀得到呢張表,或者改佢。佢存嘅係 push 裝置識別碼,令佢成為一個真實存在嘅數據外洩問題,而唔係理論上嘅問題。
我係喺一次 security review 度發現嘅。唔係喺 bug report,亦都唔係喺 testing,因為個應用由頭到尾都行得好地地。一張表冇 RLS,係唔會 throw 任何 error 嘅。啲 query 照樣成功,個功能照樣 work,而嗰個本來唔應該存在嘅權限就靜靜咁喺度,直到有人去睇,先見到佢。
佢而家已經閂咗。呢張表開咗 RLS,冇定義任何 policy,browser 角色嘅 grant 亦都已經 revoke 咗,所以嗰個以前會回傳行嘅 request,而家返嚟嘅係一個 permission error。我特登用過去式嚟講:呢個係啲 code 當時嘅樣,唔係佢今日嘅樣。
呢個就係我想講清楚嘅部分:呢個唔係我做啱咗嘅一個設計決定。呢個係我做錯咗、事後先發現嘅一次 regression。
所以我唔再信自己嘅記憶
我接下來做嘅嘢,係成件事入面我唯一夠膽交畀別人嘅部分。
我寫咗一個唔會指名任何一張表嘅 test。佢會行勻成個 schema,assert 嘅係嗰條規則:凡係喺暴露嘅 schema 入面嘅表,都一定要開 Row Level Security,而任何 tenant-scoped 嘅 materialised view,都唔可以畀 browser role 讀到。如果我下個月加咗張表又唔記得,個 test 會 fail,而且佢 fail 喺我唔記得嘅嗰樣嘢度,而唔係 fail 喺我記得嗰陣寫嘅一張清單度。
兩個誠實嘅 caveat,兩個我都寧願讀者揸住嚟質我,而唔係日後自己發現:
- 嗰項檢查入面管 materialised view 嘅一半,只覆蓋
publicschema,唔覆蓋agencyschema。agency 嗰邊有佢自己嘅暴露面,我仲未閂。 - 個 test 連唔到 database 嘅時候會 skip 自己。一個 skip 咗嘅 test 係綠色嘅,即係話一個壞咗嘅環境可以收埋一條失效緊嘅 invariant。
如果呢件事真係令我養成咗一個習慣,就係呢個:當我發現呢種形狀嘅 bug,我會去寫嗰個本來會捉到佢嘅 test——而且係對住條規則嚟寫,唔係對住嗰個 object 嚟寫。一個指名我記得嗰三個 object 嘅 test,保護嘅只係嗰三個。一個 assert 條規則嘅 test,保護嘅係我下個星期加咗、然後又唔記得咗嘅嗰一個。
第二個陷阱
仲有第二個,係我靠運氣而唔係靠設計做啱嘅,而佢正正就係我喺同 vendor 傾偈時成日聽到嘅嗰句。
Client-side filter 唔係一條邊界。如果你個應用喺 browser 度按 organisation 過濾行——.eq('org_id', ...) 之類——咁隔離就係跑喺用戶部機度,即係話用 developer tools 就刪得走。任何刪得走嘅嘢都只係顯示偏好,唔係 security control。行嘅可見性,一定要喺 database 度強制執行,或者收喺一個 caller 繞唔過嘅 server endpoint 後面。
Materialised view 就係呢個問題嗰個尷尬嘅親戚。佢哋根本冇得開 Row Level Security。喺 materialised view 上面嘅一個 SELECT grant,會回傳所有 tenant 嘅行,而佢喺 migration 入面睇落同喺一張表上面嘅同一個 grant 一模一樣——但後者仍然會畀 policy 約束住。就係因為咁,個 repo 對任何 tenant-scoped 而又 materialise 咗嘅嘢,都會 revoke browser-role grant。
我仲未搞掂嘅嗰個
Row Level Security 管唔到 TRUNCATE。佢係表級權限,唔係行級權限,所以 policy 對佢唔生效。個 repo 嘅 baseline grant 畀咗 browser 角色 ALL,覆蓋一大堆表——當中就包括 TRUNCATE——而之後再冇任何嘢將佢收窄。Materialised view 嘅 grant 係 revoke 咗嘅。呢啲唔係。所以的確有啲表,browser 角色手上揸住一個喺我啱啱喺呢一節描述過嘅邊界以外嘅權限。
實際上佢係潛在,而唔係一道打開咗嘅門:Data API 冇 TRUNCATE 呢個 verb,browser 角色亦都冇得直接連 database,而正常部署唔會將 database port 曝露出嚟。呢個係衛生問題。但佢同呢篇開頭嗰個 bug 係同一個形狀——一個靜靜咁活過咗自己理由嘅權限——而我寧願指出佢,都唔想等讀者自己撞到,然後懷疑我到底知唔知。佢需要喺兩個 schema 上面做一次 revoke,再加一個 test,assert 冇任何 browser 角色揸住一個繞得過 RLS 嘅權限。
我唔會再做嘅嘢
兩個並行嘅 schema 複製咗一大堆 DDL。 Agency 同個別生意嘅數據係兩套平行結構,而唔係一張表加一個 tenant discriminator column。呢種分離真係乾淨啲。但複製喺維護上面嘅代價,高過嗰個 discriminator column,下次我會揀另一邊。
手動 function calling 係一大堆 code。 個 agent loop 係人手處理 tool call 嘅,冇用 provider 嘅自動 function calling——好處係 streaming 同 tool execution 都留喺應用自己嘅控制入面。代價係應用要自己砌返 function-result 嘅各部分,而喺而家啲 model 上面,呢啲部分一定要帶埋 call id,淨係有 function name 唔夠。漏咗個 id,你唔會攞到 schema error。你攞到嘅嘢讀落似係個 model 發神經,而嗰個會係一個難捱得多嘅下午。如果而家有更高層嘅 API 支援帶 tool 嘅 streaming,呢筆取捨值得重新計一次。
條 migration 鏈去到 321 個檔案。 當中好大一部分叫 fix_、_v2、remove_,因為我一路係喺後面追加修正,而唔係直接改我已經寫咗嘅嘢。個 schema 嘅真實狀態,只有由頭 replay 一次 history 先知道。我當時寫嘅文章,對嗰段時間嘅信心超出咗啲 code 應得嘅程度——2025 年 11 月我話三十三個 migration「終於解決」咗 multi-tenancy,然後之後幾個月仲繼續寫修正性嘅 migration。
個 frontend 信 API 嘅 shape,但唔驗證佢。 入嚟嗰邊有小心嘅驗證——backend 用 Pydantic parse 每一個 request——出去嗰邊一個都冇。個 browser 攞到個 response,然後照信。一份共用 schema 本可以喺 field 形態變嗰陣捉到呢種 drift,而唔係等到佢變成一個白畫面先被發現。加埋上面三樣,就係架構筆記話我會唔同做法嘅嗰四樣嘢。
發布咗嘅 repo 將條 migration 鏈重建成 二十個分層 migration,按關注點分組——先係表,然後係按領域分組嘅 function、view、index、trigger、policy、grant、scheduled job,最後一輪 hardening。呢句係大致上啱,而唔係完全啱,而有邊度走樣就值得老實講:二十個入面有一個,係我自己承認用嚟放啲分唔到類嘅 function 嘅雜物袋;而表嗰一層亦都唔係照佢啲檔名暗示咁拆嘅。嗰個以 shared schema 命名嘅檔案,同時都建咗八張 agency 表;而嗰個以 agency schema 命名嘅檔案,一張表定義都冇。分層係真嘅;標籤唔完美。
呢次拆分真正做啱嘅,係讀佢嗰陣真正重要嘅嗰點:個 schema 係一次純粹嘅重組,驗證方法係將拆分前後各 dump 一次,確認兩者除咗 dump 工具嘅隨機 token 之外完全一致。
呢次驗證,就係我唯一肯郁佢嘅原因。
咩嘢生存咗落嚟
Row Level Security 係真正嘅邊界,而唔係一個開關嘅功能。一旦個設計係圍住佢嚟排,隔離就唔再係 checklist 上面一項,而係變咗系統嘅一個屬性。
寫入全部行經被路由嘅 database function。 應用 code 唔會揀要郁邊個 schema。佢 call 一個 function,由嗰個 function 去檢查 organisation 類型再 dispatch,所以個選擇只係活喺一個地方,新嘅 code path 冇得唔記得做呢個選擇。
Signup 唔理 client input。 個 provisioning trigger 唔會採納 client 提供嘅 organisation id 或者 role,因為嗰啲數據係用戶控制嘅。佢會起一個新 organisation 同一個預設 owner role。小事,容易做錯,做錯嗰陣好貴。
Client 延遲建構。 啲 service 喺第一次用到嘅時候先建佢個 provider client,而唔係喺 import 嗰陣。呢樣聽落似係風格偏好,但唔係:有好幾個 service 係喺 module import 嗰刻就建立,所以 eager construction 意味住import 個應用本身就要有 API key,而個失敗會以 provider SDK 拋出嘅一個睇唔明嘅 error 出現,早到咩都未開始行。改成 lazy,先令 demo mode 變成可能——而 demo mode,先令一個陌生人可以零成本咁評估呢個項目。
最後嗰樣係我最滿意嘅決定,而我當初做佢,唔係因為佢後來變得重要。我做佢,係為咗唔好再 crash。
如果你根本唔會睇 code
呢一節,係我仲喺 agency 嗰邊、買緊呢類軟件嗰陣會想要嘅,所以入面冇一行 code。
真正值得帶走嘅,係**「我哋按 account 過濾」同「個 database 根本回傳唔到另一個 account 嘅行」**之間嘅分別。
我成個職業生涯大部分時間都喺度買平台,最近幾年喺度自己砌,而呢條分別,正正就係我直到親手出咗錯嘅嗰一版先明白嘅。前者係 browser 跟嘅一條規則。後者係 browser 破壞唔到嘅一條規則。大部分工具只有前者,但就用後者嘅語言去形容佢。
當你喺度評估一個會裝住好幾個客戶數據嘅平台——競爭情報、成效數據、受眾定義,隨便係咩——要問嘅問題唔係「佢安唔安全」。呢個問題人人都會答安全。要問嘅係:隔離到底喺邊度執行,如果有個 developer 刪咗個 filter,會發生咩事?
好答案有兩個。佢由綁住登入用戶身份嘅 policy 喺 database 度強制執行,或者佢收喺一個 browser 繞唔過嘅 server endpoint 後面。任何答案入面只要出現 browser,就係唔得。
好嘅答案聽落似係:policy 就掛喺張表度,以用戶嘅 session 為 key;browser 從來唔會直接 query 嗰張表;呢個係證明佢嘅 test。冇乜用嘅答案聽落似係:我哋個應用按 account 過濾;啲數據係加密嘅;我哋有 SOC 2 合規。呢啲可能全部都係真,但冇一個答到條問題。
佢淨係一句,啱啱好放得入 vendor security review,而嗰度就係我會放佢嘅地方。對方嘅答案會話畀你知,multi-tenancy 係設計入去嘅,定係後來補上去嘅。
常見問題
點解要發布佢,而唔係由佢留喺 private repo?
因為公開嘅嘢可以被查證,私人嘅唔得。我早先寫過關於呢件事嘅文章,入面有啲講法;而一個帶住 migration、policy 同 test 嘅 repo,係讀者可以自己驗證嘅嘢,包括我搞錯咗嘅部分。架構筆記入面有一節叫「取捨,同埋我會點樣唔同做法」,個 repo 之所以以呢個形式存在,就係因為佢。
第二個版本會唔會開源?
唔會。佢係私下開發嘅,而個 codebase 亦都唔會似呢個。我寧願直話直說,都唔想留低模糊空間,等人 clone 咗呢個仲以為有 roadmap。
我可以用佢喺 production 度咩?
我唔會。佢係參考,唔係產品。Seed data 入面嘅 demo credentials 只係本地用,冇任何支援承諾,亦都冇人會跟住未來嘅 dependency 變化去修佢。佢係一件啱攞嚟睇、攞嚟借嘅嘢,亦都係一件唔啱攞嚟跑生意嘅嘢。
試佢係咪一定要有 AI key?
唔係。佢啟動嗰陣係 demo mode,啲 Agent 唔會 call model,而係回傳清楚標明咗嘅罐頭輸出。你可以冇 key、唔使花一蚊,撳勻成個應用——每一個 Agent、agency 嘅客戶流程、語言切換器。切去真 model call,只係一個 setting 加一條 key。我第一次跑就花咗多過十分鐘:佢要 Docker 開住,仲要 Node、Python、Poetry 同 Supabase CLI,而慢嘅部分係一個好大嘅 download。
我喺砌佢嗰陣寫咗好多關於佢嘅嘢,如果要揀兩篇開始睇,我會揀點解我第二日就砌咗 multi-tenancy同第六十七日我重做佢嗰陣發生咗咩事。如果兩篇入面只有一篇值得你花時間,就係第二篇——嗰篇係架構被證明係錯嘅嗰篇。
啲 code 喺 github.com/chandlernguyen/stratum-oss,而嗰個守住我出咗街嘅錯誤嘅 test,喺 tests/automated/test_rls_coverage.py。
如果你都發布過 multi-tenant 系統,而發現咗我冇提到嘅第三個靜默陷阱,我真係好想聽——呢啲先係值得收集嘅嘢。
反饋、建議,同埋 v1 之後會點
我唔想呢件事變成單向廣播,所以以下係「可以期待啲咩」嘅誠實版本。
我會歡迎嘅: 如果 repo 入面有啲嘢本身就係錯嘅,歡迎報 bug。邊啲地方可以更清楚、更簡單,或者用更少 code 就做到,歡迎畀建議。如果你試過將佢跑起、撞到啲 README 冇 cover 嘅嘢,歡迎話我知。如果你搵到真正嘅問題、想自己修,歡迎開 pull request。仲有,如果你 fork 咗佢、整咗自己嘅嘢出嚟,我想知你改咗啲咩、點解要改——嗰個係最有趣嘅反饋,因為啲決定係你真係做過嘅。
我可以承諾嘅: 唔多,而我寧願咁講,都唔想暗示其他嘢。呢個唔係一個仲喺積極開發嘅項目,我冇喺度營運一個 support desk,亦都冇辦法承諾回覆時間。有啲建議我會做。有啲我會睇完、認同,然後一直都冇時間做。呢個就係一個已經有咗後繼者嘅 side project 嘅現實版本。
v1 之後會點: 佢就照而家咁公開住。我唔會再繼續開發佢,所以唔好將計劃建基喺會有新 release 上面。但係我亦都唔會扮佢被封死咗——啲 code 係公開嘅,個 licence 容許你將佢帶去任何方向,而如果有啲嘢壞咗、或者真係講唔清楚,我冇理由為咗原則就由佢咁。
最簡單搵我嘅方法,係喺 repo 開個 issue;如果你唔想公開,都可以 email。
今次就講到呢度。
祝好,Chandler