In-open source ko na ngayon ang unang version ng marketing platform ko.
STRAŦUM ang pangalan nito. Siyam na AI agents, isang agency workspace na may hiwalay na data kada client, sampung wika. Ginawa ko ito noong 2025 habang tinuturuan ko ang sarili kong magsulat ng software. Tapos nagsimula ako ng pangalawang version, at sa halip na itago lang ang una, ipinamigay ko na ito.
Kung gusto mong tingnan muna bago magpatuloy sa pagbabasa: github.com/chandlernguyen/stratum-oss
git clone https://github.com/chandlernguyen/stratum-oss
Kung nagpapatakbo ka ng marketing teams at hinding-hindi mo rin lang bubuksan ang repository na iyon, dumiretso ka na sa "Kung Hindi Mo Babasahin ang Code" malapit sa dulo. Para sa iyo ang seksyong iyon, at maikli lang ito.
Gusto kong maging tiyak sa kung ano talaga ang repository na iyon, kasi maraming ibig sabihin ang "open source" at halos lahat ng mga ito ay nagpapalaki sa kung ano talaga ito.
Ang aktwal kong na-publish
Published na ang STRAŦUM v1 at hindi na ito aktibong dine-develop. Public ang code sa ilalim ng MIT licence. Walang roadmap, walang release schedule, at walang pangako ng support — nasa README iyon, at inuulit pa ito ng contributing guide.
Na-publish ito bilang isang reference implementation. Isa itong bagay na babasahin, o i-fork, o paghahanguan ng mga piraso. Hindi ito bagay na aasahan. Walang nagma-maintain nito laban sa mga pagbabago ng dependencies sa hinaharap, at nakalista sa repository ang mga open advisory na alam nito, sa halip na magkunwaring wala ang mga iyon.
Hindi ko sinasabing hindi na ito kailanman magbabago. Hindi ko na ito dine-develop pa, kaya iplanong parang mananatili ito sa kung ano ito ngayon — pero binabasa ko ang mga ipinapadala sa akin, at kung may mag-report ng bug o mag-suggest ng bagay na magpapalinaw sa code, hindi ako magkukunwaring hindi ko ito nakita.
Ang pag-publish nito ay nangangahulugan din ng pagpapatunay na walang confidential na kasamang naipadala. May script sa repository na nag-i-scan sa bawat tracked file para sa credentials, private keys, provider tokens, at production identifiers, at ito ang unang job sa CI — bago pa ang mga tests, kasi ang na-leak na key ay bahagi na ng history sa sandaling ma-push ito, at hindi na mababawi iyon kahit i-delete pa mamaya.
Heto ang mga nasa loob nito:
- Siyam na agents — strategy, persona, content, performance intelligence, competitive intelligence, campaign planning, client success, at dalawa pa. Bawat isa ay subclass ng isang base agent, at pareho silang lahat ng prompt structure, tool registry, at progressive context.
- Dalawang Postgres schemas. Nasa isa ang data ng mga indibidwal na negosyo, nasa isa pa ang data ng agency, at ang Row Level Security ang isolation boundary.
- Sampung locales, kung saan dumadaan ang locale sa API sa isang header para tumugma ang generated metadata sa wika ng interface.
- Isang demo mode na hindi kailangan ng API key. Sa halip na tumawag sa isang model, malinaw na may label na canned output ang ibinabalik ng mga agents, kaya mabubuklat mo ang buong application nang walang provider account.
Kung Gusto Mong Gumawa ng Sarili Mong Version Nito
Ito ang paggamit na pinaka-gusto kong makita, kaya sulit itong sabihin nang tapat sa halip na ipahiwatig lang.
Kung ang talagang gusto mo ay sarili mong marketing agent system — sarili mong agents, sarili mong prompts, sarili mong schema, sarili mong product sa ibabaw — i-fork ang repository at gawin ito. Para diyan ang MIT licence, at iyon ang intensyon, hindi loophole. Walang kailangang attribution maliban sa pagpapanatili ng licence file, at hindi mo kailangang magpaalam sa akin. Kunin mo ang mga bahaging kapaki-pakinabang, itapon ang hindi, at palitan ang anumang hindi mo sinasang-ayunan.
Mas gusto kong makita ang dose-dosenang iba't ibang version nito kaysa sa isang version na ako lang ang nakahawak.
Bakit i-publish ang v1 sa halip na itago ito para sa sarili ko
Sampung buwan ko itong itinago dahil inakala kong walang may gugustuhin sa isang v1 na tinigil ko na. Mali ang assumption na iyon, at ang dahilan kung bakit mali ito ay mas mahalaga pa kaysa sa repository mismo.
Iba na ang direksyon ng pangalawang version. Hindi na kamukha ng codebase na ito ang ginagawa ko ngayon, at hindi ito magiging diff laban dito. Kung private ko itong itinago, unti-unti itong magiging isang museum piece na walang makakabisita.
Ang mas magandang dahilan: ang isang gumaganang multi-tenant agentic application ay mas kapaki-pakinabang na artefact kaysa sa isang architecture diagram. Noong nag-aaral ako, hindi ang conceptual explainer ang nakatulong sa akin — ang nakatulong ay ang makahanap ng totoong project at mabasa kung paano talaga ito inayos ng iba. Ang reasoning na nawawala sa explainer ang siyang pinakamahalaga.
May apat akong isinulat sa architecture notes ng repository — mga bagay na nagawa ko nang mali at hindi ko na uulitin — at iniwan ko silang apat. Ang una ay isang security bug.
Ang pagkakamaling na-ship
May isang table na na-ship na hindi naka-on ang Row Level Security.
Ginawa ang public.notification_push_deliveries nang wala ang linyang nag-e-enable ng RLS, at binigyan ito ng full privileges sa anonymous at authenticated roles — kasama ang TRUNCATE — na wala ni isang policy na naidepina. Nasa schema ito na ine-expose ng Data API, at ang anonymous key ay kasama mismo sa browser bundle. Kaya sa panahong iyon, kahit sino na may hawak ng key na iyon ay puwedeng magbasa o mag-modify ng table. May laman itong push device identifiers, at iyon ang dahilan kung bakit data exposure talaga ito, hindi teoretikal.
Nakita ko ito sa isang security review. Hindi sa bug report, at hindi sa testing, kasi perpektong gumagana ang application sa buong panahon na iyon. Walang nag-e-error kapag kulang ng RLS ang isang table. Pumapasa ang mga query, gumagana ang feature, at ang permission na dapat ay wala naman ay nandiyan lang, walang nakikitang ginagawa hanggang may tumingin.
Sarado na ito ngayon. Naka-enable ang RLS sa table at walang policy, at na-revoke na ang grants ng browser roles, kaya ang request na dati ay nagbabalik ng rows ay bumabalik na ngayon bilang permission error. Sinasadya kong ilarawan ito sa past tense: ito ang hitsura ng code noon, hindi ang hitsura nito ngayon.
Iyon ang bahaging gusto kong maging tiyak: hindi ito isang design decision na nakuha ko nang tama. Isa itong regression na nagawa ko nang mali at natuklasan ko lang pagkatapos.
Kaya tumigil ako sa pagtitiwala sa memory ko
Ang ginawa ko pagkatapos ay ang tanging bahagi nito na buong tiwala kong maipapasa sa iba.
Sumulat ako ng test na hindi nagpapangalan ng kahit anong partikular na table. Nilalakad nito ang schema at pinapatunayan ang rule: bawat table sa isang exposed schema ay naka-enable ang Row Level Security, at walang tenant-scoped na materialised view ang nababasa ng browser role. Kapag may idinagdag akong table sa susunod na buwan at nakalimutan ko, babagsak ang test — at sa bagay na nakalimutan ko ito babagsak, hindi sa listahan na isinulat ko noong naaalala ko pa.
Dalawang tapat na caveat, at gusto kong isumbat ito sa akin ng mambabasa kaysa matuklasan pa niya mamaya:
- Saklaw ng materialised-view na kalahati ng check na iyon ang
publicschema, hindi angagencyschema. May sariling exposure ang panig ng agency na hindi ko pa nasasara. - Nilalaktawan ng test ang sarili nito kapag hindi ito makarating sa database. Berde ang isang skipped test, ibig sabihin, puwedeng itago ng sirang environment ang isang invariant na bumabagsak.
Kung may isang habitong nakuha ako sa lahat ng ito, ito iyon: kapag may nakita akong bug na ganito ang hugis, sinusulat ko ang test na sana ay nahuli nito — at sinusulat ko ito laban sa rule, hindi laban sa object. Ang test na nagpapangalan sa tatlong object na naalala ko ay nagpoprotekta lang sa tatlong iyon. Ang test na nagpapatunay sa rule ay nagpoprotekta rin sa idadagdag ko sa susunod na linggo at makakalimutan ko.
Ang pangalawang trap
May pangalawa na nakuha ko nang tama dahil sa swerte, hindi sa disenyo, at ito ang madalas kong marinig sa mga usapan sa vendor.
Ang client-side filter ay hindi boundary. Kung ang application mo ay nagfi-filter ng rows ayon sa organisasyon sa browser — .eq('org_id', ...) at mga kauri nito — tumatakbo ang isolation sa machine ng user, ibig sabihin, kayang tanggalin ito gamit ang developer tools. Anumang natatanggal ay display preference, hindi security control. Ang row visibility ay dapat ipatupad sa database, o sa likod ng server endpoint na hindi malalampasan ng tumatawag.
Ang mga materialised view ang awkward na kamag-anak ng problemang ito. Wala talaga silang kayang Row Level Security. Ang SELECT grant sa isang materialised view ay nagbabalik ng rows ng lahat ng tenant, at sa migration, kamukhang-kamukha ito ng parehong grant sa isang table — kung saan pipigilan pa rin ito ng policies. Dahil diyan mismo, binabawi ng repository ang browser-role grants sa anumang tenant-scoped na nagiging materialised.
Ang Hindi Ko Pa Natatapos
Hindi kayang pigilan ng Row Level Security ang TRUNCATE. Table-level privilege ito, hindi row-level, kaya hindi ito nasasaklaw ng policies. Ang baseline grants ng repository ay nagbibigay sa browser roles ng ALL sa napakaraming table — kasama ang TRUNCATE — at walang nagpapakipot niyon pagkatapos. Ang grants sa materialised views ay nire-revoke. Ang mga ito ay hindi. Kaya may mga table kung saan may hawak ang browser role na privilege na nasa labas ng boundary na inilalarawan ko sa buong seksyong ito.
Sa praktika, latent ito, hindi bukás na pinto: walang TRUNCATE verb ang Data API, hindi direktang makakonekta ang browser roles sa database, at hindi ine-expose ng normal na deployment ang database port. Isa itong problema sa hygiene. Pero pareho ang hugis nito sa bug sa simula ng post na ito — isang privilege na tahimik na nabubuhay nang mas mahaba kaysa sa dahilan nito — at mas gusto kong ituro ito kaysa hayaang mahanap ito ng mambabasa at mag-isip kung alam ko ba. Kailangan nito ng revoke sa dalawang schema, at isang test na nagpapatunay na walang browser role na may hawak na privilege na lumalampas sa RLS.
Ang mga hindi ko na gagawin muli
Ang dalawang parallel schema ay nag-duplicate ng maraming DDL. May parallel na structure ang data ng agency at ng indibidwal na negosyo sa halip na isang table na may tenant discriminator column. Mas malinis talaga ang paghihiwalay. Pero ang duplication ay mas malaki ang naging gastos sa maintenance kaysa sa discriminator column, at sa susunod, iba na ang pipiliin ko.
Ang manual function calling ay napakaraming code. Hinahawakan ng agent loop ang tool calls nang manu-mano sa halip na gamitin ang automatic function calling ng provider, na nagpapanatili sa streaming at tool execution sa ilalim ng kontrol ng application. Ang kapalit: kailangang i-assemble ng application mismo ang function-result parts, at sa mga kasalukuyang model, dapat dalhin ng mga part na iyon ang call id pati na ang function name. Kapag nakalimutan mo ang id, hindi schema error ang makukuha mo. Ang makukuha mo ay isang bagay na parang pabagu-bago lang ang model — at mas malala ang hapon mo kapag ganoon. Kung may higher-level API na ngayon para sa streaming with tools, sulit balikan ang trade-off na ito.
Umabot sa 321 files ang migration chain. Malaking bahagi nito ay may pangalang fix_, _v2, at remove_, kasi puro correction ang idinadagdag ko sa halip na i-edit ang mismong naisulat ko na. Ang tunay na kalagayan ng schema ay malalaman lang sa pamamagitan ng pag-replay ng history mula sa simula. Ang mga post na isinulat ko noon ay mas may tiwala sa panahong iyon kaysa sa nararapat sa code — noong Nobyembre 2025, sinabi kong "finally solved" na ng tatlumpu't tatlong migrations ang multi-tenancy, tapos ilang buwan pa akong sumulat ng corrective migrations pagkatapos.
Nagtitiwala ang frontend sa shapes ng API nang hindi ito vine-validate. Maingat ang validation sa papasok — pinaparse ng backend ang bawat request gamit ang Pydantic — at wala sa palabas. Kinukuha ng browser ang response at pinaniniwalaan ito. Mahuhuli sana ng isang shared schema ang drift kapag nagbago ang shape ng isang field, sa halip na matuklasan ito bilang isang blangkong screen. Kasama ang tatlo sa itaas, iyon ang apat na bagay na sinasabi sa architecture notes na iba ang gagawin ko.
Itinayo muli ng na-publish na repository ang migration chain bilang dalawampung layered migrations, na pinangkat ayon sa concern — tables, tapos ang mga function na pinangkat ayon sa domain, views, indexes, triggers, policies, grants, scheduled jobs, at isang huling hardening pass. Halos totoo ito, hindi eksaktong totoo, at sulit sabihin kung saan ito nagkukulang: isa sa dalawampu ang inaamin na catch-all para sa mga function na hindi ma-classify, at ang layer ng tables ay hindi nahahati sa paraang ipinapahiwatig ng mga filename nito. Ang file na pinangalanan para sa shared schema ay gumagawa rin ng walong agency tables, at ang file na pinangalanan para sa agency schema ay walang table definitions sa loob. Totoong may layering; hindi lang perpekto ang mga label.
Ang nakuha nang tama ng paghahati ay ang bahaging mahalaga para sa pagbabasa nito: ang schema ay purong reorganisation, na na-verify sa pag-dump nito bago at pagkatapos at pagkumpirma na byte-identical ang dalawa maliban sa random token ng dump tool.
Iyon lang ang dahilan kung bakit pumayag akong galawin ito.
Ang mga nanatili
Ang Row Level Security bilang aktwal na boundary, hindi bilang feature na ini-switch on. Kapag nakaayos na ang disenyo sa paligid nito, hindi na checklist item ang isolation — nagiging katangian na ito ng system.
Dumadaan ang writes sa routed database functions. Hindi application code ang pumipili kung anong schema ang gagalawin. Tumatawag ito ng function na tumitingin sa uri ng organisasyon at nagdi-dispatch, kaya nasa isang lugar lang ang pagpili — lugar kung saan hindi makakalimutan ng bagong code path na gawin ito.
Binale-wala ng signup ang client input. Hindi sinusunod ng provisioning trigger ang organisation id o role na galing sa client, kasi kontrolado ng user ang data na iyon. Bumubuo ito ng bagong organisasyon at default owner role. Maliit na bagay, madaling mali, mahal kapag nagkamali ka.
Lazy client construction. Ginagawa ng mga service ang provider client nila sa unang paggamit, hindi sa import. Mukha itong style preference pero hindi: may ilang service na nagagawa na sa module import, kaya ang eager construction ay nangangahulugan na kailangan ng API key para ma-import lang ang application, at lumalabas ang failure bilang malabong error mula sa SDK ng provider bago pa may makapagsimula. Ang paggawa nito na lazy ang nagpapagana sa demo mode, at iyon ang nagpapahintulot sa isang estranghero na suriin ang project nang walang gastos.
Iyon ang desisyong pinakamasaya ako — at hindi ko ito ginawa para sa dahilang naging mahalaga pala ito. Ginawa ko ito para itigil ang crash.
Kung Hindi Mo Babasahin ang Code
Ito ang seksyong hinahanap ko sana noong nasa panig ako ng agency at bumibili ng ganitong software, kaya wala itong code.
Ang dapat dalhin mula rito ay ang pagkakaiba ng "nagfi-filter kami ayon sa account" at "hindi kayang ibalik ng database ang rows ng ibang account."
Halos buong career ko ay bumili ako ng platforms, at nitong huling ilang taon ay ako naman ang gumagawa ng mga ito, at ang pagkakaibang iyon ang hindi ko naunawaan hanggang sa na-ship ko ang maling version nito. Ang isa ay rule na sinusunod ng browser. Ang isa ay rule na hindi kayang labagin ng browser. Nasa karamihan ng tools ang una, at ikinukuwento nila ito sa pananalita ng pangalawa.
Kapag sinusuri mo ang isang platform na magtatago ng data ng ilang client — competitive intelligence, performance data, audience definitions, kung ano man iyon — ang tanong ay hindi "secure ba ito." Lahat sinasabing oo diyan. Ang tanong ay: saan ipinatutupad ang separation, at ano ang mangyayari kapag tinanggal ng isang developer ang filter?
May dalawang magandang sagot. Ipinatutupad ito sa database sa pamamagitan ng policies na nakaugnay sa identity ng naka-sign in na user, o ipinatutupad ito sa likod ng server endpoint na hindi kayang lusutan ng browser. Anumang sagot na may kinalaman ang browser ay hindi.
Ganito ang tunog ng magagandang sagot: nasa table ang policy at naka-key sa session ng user; hindi kailanman direktang nag-query ang browser sa table na iyon; heto ang test na nagpapatunay nito. Ganito naman ang tunog ng hindi gaanong kapaki-pakinabang na sagot: nagfi-filter ang application namin ayon sa account; naka-encrypt ang data; SOC 2 compliant kami. Maaaring totoo ang lahat ng iyon, at wala ni isa ang sumasagot sa tanong.
Isang linya lang ito, at kasya ito sa isang vendor security review — doon ko ito isasama. Sasabihin sa iyo ng sagot kung ang multi-tenancy ay kasama sa disenyo mula sa simula o idinagdag lang mamaya.
Mga Madalas Itanong
Bakit i-publish ito sa halip na hayaang nakatago sa isang private repository?
Kasi may makakapag-check sa isang public na artefact, at wala sa private. May mga claim ang mga nauna kong post tungkol sa paggawa nito; ang repository na may migrations, policies, at tests ay isang bagay na kayang i-verify ng mambabasa, kasama pa ang mga bahaging nagawa ko nang mali. May seksyon sa architecture notes na tinatawag na "Tradeoffs, and what I would do differently," at iyon ang dahilan kung bakit ganito ang anyo ng repository.
Magiging open source ba ang pangalawang version?
Hindi. Pribado itong dine-develop, at hindi inaasahang magkamukha ang codebase nito at ang isang ito. Mas gusto kong sabihin iyon nang tapat kaysa hayaang malabo at mag-clone ang mga tao nito nang umaasa ng roadmap.
Puwede ko bang gamitin ito sa production?
Hindi ko gagawin. Reference ito, hindi product. Ang demo credentials sa seed data ay para sa local use lang, walang pangako ng support, at walang nag-aayos nito laban sa mga pagbabago ng dependencies sa hinaharap. Magandang bagay itong basahin at hiramin, at masamang bagay na pagpatakbuhan ng negosyo.
Kailangan ko ba ng AI key para subukan ito?
Hindi. Nagsisimula ito sa demo mode, kung saan malinaw na may label na canned output ang ibinabalik ng mga agents sa halip na tumawag sa isang model. Mabubuklat mo ang buong application — bawat agent, ang agency client flows, ang language switcher — nang walang key at walang gagastusin. Isang setting at isang key lang ang paglipat sa totoong model calls. Umabot ako ng mahigit sampung minuto sa unang subok: kailangan nitong tumatakbo ang Docker, Node, Python, Poetry, at ang Supabase CLI, at ang mabagal na bahagi ay isang malaking download.
Ang dami kong naisulat tungkol sa paggawa nito habang ginagawa ko ito, at ang dalawang post na uunahin ko ay ang bakit gumawa ako ng multi-tenancy sa day two at ang nangyari nang buuin ko itong muli sa day sixty-seven. Kung isa lang sa kanila ang sulit sa oras mo, iyon ang pangalawa — iyon ang post kung saan napatunayang mali ang architecture.
Nasa github.com/chandlernguyen/stratum-oss ang code, at ang test na nagbabantay sa pagkakamaling na-ship ko ay nasa tests/automated/test_rls_coverage.py.
Kung nakapag-ship ka na ng multi-tenant system at may natuklasan kang pangatlong silent trap na hindi ko nabanggit, gusto ko talagang marinig iyon — iyon ang mga sulit kolektahin.
Feedback, Suggestions, at ang Mangyayari sa v1
Mas gusto kong hindi ito maging one-way broadcast, kaya heto ang tapat na bersyon ng dapat mong asahan.
Ang malugod kong tatanggapin: mga bug report kung may bagay sa repository na mali talaga. Mga suggestion sa mga bahaging puwedeng maging mas malinaw, o mas simple, o gawin sa mas kaunting code. Mga note mula sa kahit sino na sinubukang patakbuhin ito at may naabutang hindi sakop ng README. Mga pull request, kung may nakita kang tunay na problema at gusto mong ayusin ito. At kung i-fork mo ito at gumawa ng sarili mo, gusto kong malaman kung ano ang binago mo at bakit — iyon ang pinaka-interesanteng feedback sa lahat, kasi kinailangan mong gawin ang mga desisyon nang totohanan.
Ang maipapangako ko: hindi ganoon karami, at mas gusto kong sabihin iyon kaysa magpahiwatig ng kabaligtaran. Hindi ito actively developed na project, wala akong pinapatakbong support desk, at hindi ako makakapangako ng response time. May mga suggestion na gagawin ko. May mga babasahin ako, sasang-ayunan, at hindi na maaabot. Iyon ang makatotohanang bersyon ng isang side project na may successor na.
Ang mangyayari sa v1: mananatili itong published kung ano ito. Hindi ko na ito dine-develop pa, kaya huwag kang mag-plan base sa mga bagong release. Pero hindi ko rin ipagkukunwaring selyado ito — public ang code, pinapayagan ka ng licence na dalhin ito sa kahit anong direksyon, at kung may sirang bagay o tunay na malabo, wala akong magandang dahilan para iwanan ito nang ganoon dahil lang sa prinsipyo.
Ang pinakasimpleng paraan para maabot ako ay isang issue sa repository, o email kung ayaw mong maging publiko tungkol dito.
Iyon lang muna mula sa akin.
Maraming salamat, Chandler