Why am I Building an Open-Source Marketing Platform Powered with AI
Almost every marketing agency I've worked with uses exactly the same chaotic stack: a SaaS for scheduling social media, the Google Ads dashboard open in another tab, ChatGPT in a third window for copy ideas, and somewhere in there that infamous spreadsheet trying to tie it all together. None of these tools knows the others exist, none of them remembers the client's brand voice, and—most importantly—none of them can act on the data.
They just display the information and hope you know what to do next.
I spent years watching this pattern repeat itself. The time lost copying and pasting between disconnected systems is absurd. So I started asking a question:
Why isn't there a single platform that manages all of this, with AI that has real context about the client?
The answer is uncomfortable: it's genuinely hard to build this correctly. The SaaS market has little incentive to solve it when selling five separate subscriptions to an agency is more profitable than one integrated platform.
So I got started.
Note: Obviously, this is a problem that doesn't technically exist, and I'm about to solve it—this is the moment where either 1. I build a great platform that nobody uses, or 2. I build a great, disruptive platform. At the end of the day, there's no way to lose, aside from many hours of my life.
- You can access Meisterfy public repository by clicking here
- You can access Meisterfy webpage by clicking here
So what the heck is Meisterfy, exactly?
Meisterfy is a self-hosted, open-source marketing management platform built for agencies. In a single application, you manage:
- Social Media: A visual calendar to plan, approve, and publish posts on Facebook and Instagram. AI-assisted draft generation with your client's brand identity baked in—not just a prompt, but the persona, the tone, the hashtags, the niche, and the target language injected into every generation call.
- Google Ads: Campaign monitoring, ad group management, search term analysis, bidding criteria, and 180-day metric retention for richer historical analysis. Blend real-time API data with locally stored history for reports that Google's own console can't produce.
- AI Content and Reports: AI-generated social posts and automated campaign reports. This isn't generic AI—the tenant's real brand identity is part of the system prompt. The AI knows who it's writing for.
- Native MCP Server: An in-process Model Context Protocol (MCP) server that lets any external AI agent—like Claude Desktop, ChatGPT, Gemini, any MCP-compatible IDE extension, or custom automation—authenticate with a scoped API key and interact with your marketing data programmatically.
- Multi-Tenant by Design: Each client is a tenant. Each tenant has its own connections, brand identity, users, roles, and permissions.

Architecture with Go + SvelteKit?
A lot of people are probably thinking, "S-sv-sv-sve-Svelte???"—easy there, young Padawan! Every architectural decision has a reason, and the choices reflect the state of the art in 2026.
Go 1.25, chi, pgx/v5, sqlc, and goose
Go was the only reasonable choice for this kind of platform. The requirements were clear: a language that compiles to a single binary, has a small and predictable memory footprint, handles concurrent I/O efficiently, and has excellent static analysis tooling. With the release of Go 1.25 and 1.26, the toolchain and runtime have never been more robust.
And the second reason, which I should make clear, even though it has no real bearing on this context. I've been using PHP/Laravel for decades, I'm studying Go and Rust, and I'm loving it. Time to put some things into practice—and that's exactly why I ran away from ORMs and any kind of magic. If I was going to keep that structure, I'd just stick with the excellent Laravel.
- chi is minimal and composable: no magic! Just explicit middleware chains. You can trace exactly what runs on any route by reading the router definition.
- pgx/v5 uses Postgres's native protocol, supports connection pooling via
pgxpool, and avoids the abstraction overhead ofdatabase/sql. - sqlc generates type-safe Go code from raw SQL queries. I write SQL, sqlc generates the Go functions. No ORM, no
interface{}at the database boundary, no runtime query-composition surprises. Every query is validated at build time. - goose manages schema migrations sequentially with explicit
up/downSQL files. Every schema change is tracked, reversible, and reproducible. Thecmd/migratebinary runs goose against the target database: the same code in development, CI, and production.
SvelteKit 5, Svelte Runes, Tailwind v4, Paraglide.js, and Bun
- SvelteKit 5 with Svelte Runes is the most ergonomic reactive frontend stack available right now—and one that nobody uses 🫣. Runes replace the implicit
$:reactivity system with explicit primitives—$state,$derived,$effect. This isn't a cosmetic change; in a complex application with multi-step approval workflows, real-time AI streaming responses, and calendar-based planning interfaces, implicit reactivity creates invisible dependency chains that are impossible to reason about at scale. Explicit Runes eliminate that class of bug entirely, and benchmarks show that Svelte 5 outperforms other reactivity libraries by a solid margin. - Paraglide.js generates fully type-safe i18n message functions at build time. Every translation key is a TypeScript function. If a key exists in English but is missing in Portuguese, the CI's i18n audit step fails the build. No silent missing translations in production.
- Bun as the package manager and test runner replaces npm/yarn and Jest. Faster installs, faster test runs.
Single Binary
This is the most beautiful part of Go, and it means: zero runtime dependencies. For production, the Go binary embeds the compiled SvelteKit SPA using Go's embed package:
//go:embed cmd/server/ui/distvar uiFS embed.FSOne binary, one process, and no Node.js runtime required on the server. You can run Meisterfy on a $5 VPS with Docker and PostgreSQL. That's it.
Multi-Tenancy first
Every object in the system is tenant-scoped. The database schema enforces this at the constraint level—cascading foreign keys, not application-level filtering.
CREATE TABLE posts ( id TEXT PRIMARY KEY, -- ULID, lexicographically sortable by time tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'approved', 'scheduled', 'published')), content TEXT NOT NULL DEFAULT '', hashtags JSONB NOT NULL DEFAULT '[]', platforms JSONB NOT NULL DEFAULT '[]', workflow JSONB, ...);
CREATE INDEX idx_posts_scheduled ON posts (tenant_id, scheduled_date) WHERE status = 'scheduled';The primary keys are ULIDs, not UUIDs. ULIDs are lexicographically sortable by creation time; range queries and pagination on ULIDs are efficient without needing a separate created_at index, since the key itself encodes temporal order. Each tenant has a complete brand identity object: primary persona, tone of voice, niche, default language, and default hashtags. This identity is injected into every AI generation call as part of the system prompt. Content generation follows the guidelines defined for the brand, considerably increasing delivery quality with a lower token cost.
As for permissions, a user can be an admin in tenant A and a reader in tenant B. Permissions are seeded from SQL migrations and enforced at the repository layer—not just at the HTTP handler boundary. Permissions are so standard and mandatory in today's market that I need to extract this into reusable code for other projects.

The Connector System
This is my favorite part of the project, and to me, its very heart. Every external service—like Google Ads, Meta, S3, R2, OpenAI, Claude, Gemini, Kimi, Groq, Resend, Brevo, Sentry, and so on—goes through a unified connector system. Credentials live in the integrations table, encrypted at rest with AES-256-GCM:
// Encrypt encrypts plaintext using AES-256-GCM with a random nonce.// Returns base64(nonce || ciphertext || GCM auth tag).func Encrypt(key []byte, plaintext string) (string, error) { block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } sealed := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(sealed), nil}The nonce is randomly generated per encryption call, the GCM authentication tag provides integrity verification, and a tampered ciphertext is rejected. The encryption key is derived from your JWT_SECRET, a value you define that never leaves your server. Your Google Ads refresh token, your Meta OAuth credentials, your OpenAI API key... none of it is readable by anyone other than your own running instance.
A single integration can be assigned to multiple tenants: one Google Ads connection can serve five different client accounts without duplicating credentials; revoke the integration and it disappears from all tenants simultaneously.
MCP on day one
Yes! This is where things get more interesting.
This is the feature that makes Meisterfy structurally different from everything else in the marketing tooling space. In 2026, the Model Context Protocol has become the "missing AI layer," standardizing how AI applications communicate with data infrastructure. The MCP market is hot, practically every AI has this integration, and hundreds upon hundreds of tools use it.
What MCP Is
In practice, MCP is an API, and nothing more!
But, in robot language, it carries an extra impact: MCP is an open standard that defines how AI agents communicate with external tools over HTTP. Any MCP-compatible client—be it Claude Desktop, GitHub Copilot, VS Code extensions, Cursor, or custom automation scripts—can discover tools, read resources, and call actions on an MCP server using a well-defined JSON-RPC protocol.
Today, practically no marketing SaaS exposes an MCP endpoint. Your AI agent can read a screenshot of a dashboard and fill in a form. That's the limit, and it creates a huge bottleneck for companies trying to move from simple chat prompts to advanced automated workflows.
Meisterfy's MCP Server
Instead of using a third-party library, the best option for 100% control and maintainability is to build the MCP server from scratch inside the Go application. This keeps the dependency surface minimal and gives me full control over the HTTP Streamable transport layer.
type Server struct { name string version string tools map[string]*toolDef resources map[string]*resourceDef}
func (s *Server) RegisterTool(name, description string, schema map[string]any, handler func(ctx context.Context, args json.RawMessage) ToolResult) { s.tools[name] = &toolDef{ Name: name, Description: description, InputSchema: schema, handler: handler, }}The MCP endpoint is mounted at /mcp, separate from the JWT-protected REST API. Authentication uses scoped API keys—readonly, editor, or admin—generated and revoked directly from the tenant's settings UI. Each key is tied to a specific tenant, so the agent's context is always correctly scoped. This aligns perfectly with the enterprise security requirements for MCP, which emphasize isolated environments and explicit context declarations to avoid "tool poisoning" and unauthorized access.
Tools Available to Agents
Many! Among them: various live and monitoring metrics, content management, campaign management, and so on.
s.RegisterTool("get_live_metrics", "Get live campaign metrics from Google Ads API", map[string]any{"type": "object", "properties": map[string]any{}}, func(ctx context.Context, _ json.RawMessage) mcp.ToolResult { tenantID, ok := mcp.TenantIDFromContext(ctx) if !ok { return mcp.ErrResult("tenant not authenticated") } client, _, err := factory(ctx, tenantID) if err != nil { return mcp.ErrResult(err.Error()) } metrics, err := client.GetLiveMetrics(ctx) if err != nil { return mcp.ErrResult(err.Error()) } return mcp.Ok(metrics) },)What does this mean in practice? Connect Claude Desktop to Meisterfy with an editor key, and you can have a conversation like:
"Check all active campaigns for client X. Which keywords have a conversion rate below 1% and are spending more than 15% of the campaign budget?"
The agent pulls real-time metrics, runs the analysis, and presents the answer. Then:
"Add those keywords as negatives and reduce the campaign budget by 10%."
The agent calls get_campaign_criteria, adjust_campaign_budget, and confirms the change, all in the same conversation, all from real data. This is the shift from "Integrations as a Service" to "Context as a Protocol." It's still possible to have conversations seeking insight—the sky's the limit here.

Proposals! Not Guesses
The Adjustment Engine is an internal package that evaluates campaign performance and generates bid or budget adjustment proposals. It's proposal-based, not autonomous; each proposal is recorded in the audit trail, and the operator decides whether to apply it.
The engine applies hard guardrails before generating any proposal:
// Guardrail 1: campaign age — don't touch campaigns younger than 14 daysif time.Since(createdDate) < minAge { return nil, nil // skip silently}
// Guardrail 2: adjustment interval — don't readjust within less than 7 daysif time.Since(lastAdjusted) < interval { return nil, nil}
// Guardrail 3: minimum data — needs at least 3 days with data in the last 7 dayssince := time.Now().AddDate(0, 0, -7)allMetrics, err := e.metrics.GetHistory(ctx, resource.TenantID, since)
// ...and so onIn this context, a campaign younger than 14 days is ignored: it hasn't accumulated enough data to make a meaningful adjustment. A campaign adjusted less than 7 days ago is ignored, since repeated changes destabilize the learning period. A campaign without enough metric history generates no proposal.
These guardrails are configurable per tenant: MinCampaignAgeDays, AdjustmentIntervalDays, MaxIncreasePct, MaxIncreaseBRL, MaxDecreasePct, MaxDecreaseBRL. The engine operates within a strictly bounded budget envelope. It has no unlimited authority.
This is the difference between automation that is trustworthy and automation that is dangerous.
Testing Strategy: Coverage That Means Something
I have strong opinions about testing. A coverage number is only meaningful if the tests are actually exercising the right failure modes. For an open-source project chasing enterprise adoption, stability is paramount.
Backend Integration Tests: 86.5% Repository Coverage
The integration tests run against real PostgreSQL. I use fergusstrange/embedded-postgres to spin up an in-process Postgres 16 instance for unit test runs, and a real postgres:16-alpine container in CI.
No mocks. Every repository method—Create, Update, List, GetByID, Delete—runs against a database with real migrations applied. The integration build tag separates these from unit tests so they can be run in parallel appropriately:
go test -tags=integration -race -count=1 -p 1 ./...The -race flag runs Go's race detector on every test. The -count=1 flag disables the test cache—flaky tests that pass due to stale cache state don't get to hide.
Backend Unit Tests
The unit tests cover the cryptography package (AES-256-GCM encryption/decryption round trip, tampered ciphertext rejection), JWT issuance and validation, bcrypt password hashing, and HTTP handler contracts. All with -race.
Smoke Tests
7 smoke tests verify that the built, running binary responds correctly to: GET /health, POST /auth/login with invalid credentials (401), GET /admin/* without a token (401), and POST /mcp with an invalid key (401). These run in CI against a binary that was built from source in the same pipeline run.
Frontend: 260+ Tests Across Three Layers
- 159 Vitest tests — full CRUD coverage for each API client module. Every request format, every error path.
- 99 browser component tests — Playwright + Vitest running in a real Chromium browser. Component behavior, not just rendering.
- 10 Playwright E2E tests — authentication flows, protected route enforcement, full golden-path post creation including the approval flow.
The E2E suite requires a running stack. In CI, the full stack is started via Docker Compose, migrations are run, a test user is seeded, and Playwright runs against a live instance. The HTML report is uploaded as a GitHub artifact on failure.
CI/CD
Every push and pull request to main triggers the full pipeline. Jobs run in parallel where dependencies allow:
all-checks: name: All Checks Passed if: always() needs: - go-lint # golangci-lint - go-build # go build + go vet - go-test-unit # unit tests with race detector - go-test-integration # repository layer against real PostgreSQL - go-security # govulncheck — executable call-graph analysis - smoke-test # 7 contract tests against running binary - frontend-quality # eslint + tsc + i18n audit - frontend-test # 260+ vitest + playwright tests - frontend-build # bun run build steps: - name: Check all jobs run: | results='${{ toJSON(needs) }}' if echo "$results" | grep -qE '"result":"(failure|cancelled)"'; then echo "One or more jobs failed or were cancelled" exit 1 fiThe gate job aggregates all the results. A single cancelled or failed job causes the gate to fail, and the PR can't be merged. go-security runs govulncheck, Go's vulnerability scanner. Unlike go audit, govulncheck traces the actual call graph of the compiled binary and reports only vulnerabilities in functions that are reachable from your code. frontend-quality runs ESLint, strict TypeScript checking (svelte-check), and a custom i18n audit script that checks locale parity—if a key exists in English but is missing in Portuguese (or vice versa), the build fails. The e2e pipeline (e2e.yml) is separate; it runs on push to main and on manual dispatch, starting the full Docker stack (Postgres, migrations, Go binary, SvelteKit SPA) and running the Playwright suite.
Is the Market Gap Real?
Well, at least from what I've researched... it is! My points:
- No unified social + ads platform with real AI context: Tools like Hootsuite and Buffer handle social scheduling. The Google Ads console is a data viewer. There is no single platform that contains both, with a shared brand identity driving the AI across both surfaces. This is due to several factors, but my guess is that they're too big for certain risks—astronomically different from our case.
- No marketing platform with a native MCP server: Almost no marketing SaaS exposes an MCP endpoint. This means that AI agents—the tools that engineering teams are actively building and integrating—can't interact programmatically with marketing data; they read screenshots and fill in web forms. That's the limit. Meisterfy raises that bar: agents that read real-time data, generate content, modify campaigns, and interact with your brand identity in a single authenticated session.
- No open-source, self-hosted alternative for agencies: Here I'm certain—every marketing tool for agencies is SaaS. Per-seat pricing, vendor lock-in, no control over your data, no ability to self-host or customize. While there are open-source projects for marketing automation (like the excellent Mautic), Meisterfy is the only fully open-source platform in this space that unifies social, ads, and AI context.
What's Coming Next
Well, if you've had the patience to read this massive longform all the way here, thank you so much! Meisterfy is targeting an alpha launch in late 2026. Development is active and ongoing.
The source code is on GitHub. The architecture is already designed at a production level, and the test suite is comprehensive. The CI/CD pipeline runs 11 jobs on every PR and doesn't let anything questionable through. If you're building AI automation for marketing workflows, or you simply want a robust monitoring and management platform, keep an eye on this project.