vibecms

CMS for AI Agents.

The CMS your agents publish into: Markdown posts, versions, and media over scoped MCP or the REST `/api/v1` API - you own every post. Every mutation creates activity, and meaningful post changes create versions. ## Features - Clean hosted blog dashboard - Public blog pages - Markdown post editor - R2 media uploads - D1 database - Activity history - Post version history - Scoped agent tokens with `vc_` prefixes - MCP endpoint for trusted agents - Polar billing for hosted vibecms Cloud - `SELF_HOSTED=true` mode without Polar ## Connect an MCP client vibecms exposes standards-based MCP over Streamable HTTP. In the dashboard, open **Connect**, create a scoped token, and copy it once. Claude Code is the primary example: ```sh claude mcp add --transport http vibecms https://your-vibecms-domain.com/mcp \ --header "Authorization: Bearer vc_..." ``` Any compatible MCP client uses the same endpoint and bearer credential: ```json { "mcpServers": { "vibecms": { "type": "http", "url": "https://your-vibecms-domain.com/mcp", "headers": { "Authorization": "Bearer vc_..." } } } } ``` Install the client-independent safety and writing skills: ```sh npx skills add moinulmoin/vibecms --skill vibecms-core --skill vibecms-writing ``` Verify credentials with a protected read—not `tools/list`, which is intentionally available for tool discovery: ```sh curl https://your-vibecms-domain.com/mcp \ -H "Content-Type: application/json" \ -H "Authorization: Bearer vc_..." \ --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"sites.get","arguments":{}}}' ``` A valid token returns the current site in `structuredContent`; an invalid or missing token returns `401`. The approval-first publishing flow is: ```txt sites.get -> posts.format_guide -> draft -> posts.preview -> latest saved version -> explicit approval -> posts.publish(postId, expectedVersionNumber) -> returned URL ``` The REST `/api/v1` API mirrors the MCP tools with full read/write parity. Post lists return summaries without full Markdown, so fetch a single post body through REST or MCP `posts.get`: ```sh curl "https://your-vibecms-domain.com/api/v1/posts?limit=20&offset=0" \ -H "Authorization: Bearer vc_..." ``` Hosted vibecms Cloud counts MCP and REST against the same workspace API quota. Rate-limit failures are machine-readable: REST returns `429` with `RATE_LIMIT`, and MCP returns a JSON-RPC rate-limit error. ## License vibecms is licensed under AGPL-3.0-or-later. See `LICENSE`. The vibecms name and marks are covered by the trademark guidelines in `TRADEMARKS.md`. ## Scripts ```sh pnpm install pnpm typecheck pnpm lint pnpm test pnpm build pnpm public:audit pnpm db:migrate:local pnpm db:seed:local pnpm dev ``` Database migration SQL lives in `packages/db/drizzle/` and is shared by both Workers. Local API development, Astro rendering, migrations, and seed commands share the root `.wrangler/state` directory so both Workers see the same D1 and R2 data. See `MILESTONES.md` for the milestone-by-milestone build plan and acceptance checks. ## Self-host mode vibecms now has a real self-host switch: ```txt SELF_HOSTED=true ``` In self-host mode, Polar is optional, billing gates are disabled, and hosted workspace API quotas are not enforced by default. After signup and blog setup, the owner lands on `/dashboard`; publishing, media uploads, scoped agent access, activity history, and post versions run on the self-hoster's Cloudflare D1/R2 resources. The repository deploys the same two-Worker topology in hosted and self-hosted modes: - `apps/api/wrangler.jsonc` and `apps/public/wrangler.jsonc` configure hosted development/production. - Root `wrangler.jsonc` configures the self-hosted Hono API + dashboard Worker. - Root `wrangler.public.jsonc` configures the self-hosted Astro public-blog Worker. - `pnpm deploy` builds both, applies D1 migrations, then deploys API before public. Deploy button shape, once this repo is public: ```md [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/moinulmoin/vibecms) ``` During deploy, configure both root Wrangler files. Public blogs are host-only (tenant identity is the host), so the API/dashboard and public blog use separate Worker hosts: ```txt APP_URL=https://vibecms..workers.dev BETTER_AUTH_URL=https://vibecms..workers.dev PUBLIC_BLOG_DOMAIN=vibecms-public..workers.dev SELF_HOSTED=true ``` The public blog serves at `/`, `/`, and `/tag/` on `PUBLIC_BLOG_DOMAIN`; removed `/blog//*` path mode is not supported. The only required self-host secrets are listed in `.dev.vars.example`: ```txt BETTER_AUTH_SECRET= TOKEN_PEPPER= ``` Minimal local/self-host env shape (local URLs are not public hosted URLs): ```txt APP_URL=https://vibecms.example.workers.dev BETTER_AUTH_URL=https://vibecms.example.workers.dev PUBLIC_BLOG_DOMAIN=vibecms-public.example.workers.dev SELF_HOSTED=true BETTER_AUTH_SECRET= TOKEN_PEPPER= ``` See `docs/self-hosting.md` for the Cloudflare self-host flow and deploy-button notes. ## Launch notes - Configure shared Cloudflare D1/R2 IDs in `apps/api/wrangler.jsonc` and `apps/public/wrangler.jsonc` before production deploy. - Set API Worker secrets with Wrangler: `BETTER_AUTH_SECRET`, `TOKEN_PEPPER`, `POLAR_ACCESS_TOKEN`, and `POLAR_WEBHOOK_SECRET`. - Set product, URL, and host variables in both Worker configs. - Run `pnpm deploy:prod`; it preflights and builds production artifacts, captures backup metadata, migrates D1, deploys API then public, then smokes. Astro sessions are disabled (no SESSION KV). For self-hosted production, set `SELF_HOSTED=true` and only `BETTER_AUTH_SECRET` plus `TOKEN_PEPPER` are required as secrets; Polar access token/product/webhook secrets are hosted-SaaS only. ## Dev deployment Current Cloudflare development resources are wired in `apps/api/wrangler.jsonc` and `apps/public/wrangler.jsonc`: - API + dashboard Worker: `vibecms-api-dev` at `https://app.basedui.dev` - Public Astro Worker: `vibecms-public-dev` at `https://basedui.dev` and `*.basedui.dev` - Shared D1 database: `vibecms_dev` - Shared R2 bucket: `vibecms-assets` Run the full dev deploy/test flow: ```sh pnpm install pnpm typecheck pnpm lint pnpm db:seed:dev pnpm deploy:dev ``` `pnpm deploy:dev` syncs the development token pepper, applies remote D1 migrations, builds the dashboard assets, deploys the Hono API Worker, then builds and deploys the Astro public Worker. For Polar billing, create a sandbox product in Polar and update: ```sh # In apps/api/wrangler.jsonc, replace product_dev_placeholder: # "POLAR_PRODUCT_ID": "" pnpm --filter @vc/api exec wrangler secret put POLAR_ACCESS_TOKEN pnpm --filter @vc/api exec wrangler secret put POLAR_WEBHOOK_SECRET pnpm deploy:dev ``` Recommended sandbox product setup: - Recurring subscription product. - Price: $19/month, or $190/year if you create a yearly product/price in Polar. - Set the monthly product as `POLAR_MONTHLY_PRODUCT_ID`. If yearly is a separate Polar product, set it as `POLAR_YEARLY_PRODUCT_ID`. - In hosted mode, new workspaces stay behind the Polar checkout gate until checkout/webhooks mark billing active. In self-host mode, `SELF_HOSTED=true` bypasses billing gates entirely. - Launch entitlement: 1 hosted blog, unlimited posts, 5 GB media, scoped agent access, activity and version history, and reader + AI discovery analytics. - Upload policy enforced by the app: JPEG/PNG/WebP/GIF only, 10MB max image size, no video hosting, no generic file hosting. Recommended minimum Polar organization access token scopes: - `checkouts:write` for creating checkout sessions. - `customer_sessions:write` for creating customer portal sessions. You do not need product, order, refund, file, meter, webhook, or subscription write scopes for the current app runtime. Webhooks are verified with `POLAR_WEBHOOK_SECRET`, not the API token. In Polar, set the webhook endpoint to: ```text https://app.vibecms.dev/polar/webhook ``` Subscribe to these webhook events: - Required: `subscription.created`, `subscription.updated`, `subscription.active`, `subscription.past_due`, `subscription.canceled`, `subscription.revoked`. - Required for checkout/customer reconciliation fallback: `checkout.updated`. - Optional but useful for analytics later: `order.paid`. The app currently updates billing state from `subscription.*` payloads and from successful `checkout.updated` payloads. Keep the webhook delivery format as raw JSON and copy the endpoint signing secret into `POLAR_WEBHOOK_SECRET`.