Vibecode Circle
track this build5 phases, 15 steps, beginner friendly0%A private forum/community is buildable, but Circle's value is member management, courses, events, payments, mobile apps, automations, AI, and migrations.
You are building a lean indie version of Circle. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Circle · indie build Discourse self-hosted as the community itself, with private categories as spaces, a small membership service that takes Stripe subscriptions and grants or revokes group access as they start and lapse, single sign-on so members never manage a second password, and email notifications that actually arrive. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Community | Discourse, self-hosted with its official install | posts, threading, moderation, search, trust levels and spam handling, none of which you should write | | Membership | A small Node 22 service alongside it | the only genuinely custom part: subscriptions in, group membership out | | Billing | Stripe Checkout and the customer portal, driven by webhooks | hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build | | Identity | Discourse as the single sign-on provider for your membership pages | one account, and the forum stays the source of truth for who someone is | | Email | A transactional provider with a verified sending domain | a community whose notifications land in spam is a dead community | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server meeting Discourse's requirements** · typically 10 to 30 USD a month for a small community - Why: Discourse is not lightweight, and an undersized server produces a community that is slow in a way members notice immediately. - Get it: Check the current official requirements before choosing, then provision accordingly. Its own installation guide is the authority on the current figure. - Verify: The official installer completes and the site loads - [ ] **A domain with DNS you control** · roughly 10 to 15 USD a year - Why: Needed for the site, the certificate and the email sending domain records. - Get it: Any registrar. You will add A records and several email verification records. - Verify: The hostname resolves and the email records validate at your provider - [ ] **A transactional email provider with a verified sending domain** · free tiers typically cover a few thousand messages a month; a busy community will exceed that - Why: Discourse is unusable without it. Signup, invitations, password reset and every notification depend on outbound mail arriving. - Get it: Create an account, add the DNS records they specify for domain verification, and take the SMTP credentials. Verification propagation is the slow step, so start it first. - Verify: A test message from the server reaches an inbox and is not marked spam - [ ] **A Stripe account with test mode keys** · free in test mode; live payments cost per transaction plus a further fee on subscriptions in most regions - Why: Phase 3 needs the secret key and a webhook signing secret. Test mode does everything except move money. - Get it: Sign up at stripe.com, take the keys from Developers then API keys, and get the webhook secret from the endpoint you register or from the listen command. - Verify: stripe listen prints a signing secret and a triggered event reaches your endpoint - [ ] **A Discourse admin API key** · free - Why: The membership service adds and removes group members through it. It is a powerful credential and belongs only on the server. - Get it: Create it in the Discourse admin API settings, scoped as narrowly as the platform allows. - Verify: A group membership change through the API succeeds - [ ] **A moderation policy and someone to apply it** · free, except for the time it will take you every week afterwards - Why: A paid community with no policy makes one up in its first crisis, which is the worst time to write it. - Get it: Write what is not allowed, who decides and how someone appeals, before you invite anyone. One page is enough. - Verify: The policy is written and the platform settings reflect it ## Quick start ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. - polished member UX - mobile apps - payments - course/events integration - automations - migrations - community admin If one of those is essential to you, that is the reason to keep paying for Circle, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Circle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private paid community with spaces, roles, membership billing and notifications to replace Circle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a forum engine. Discourse already handles posts, threading, moderation, trust levels, search, spam and the thousand details that make a community usable, and it has done so for over a decade. Deploy it, then build the layer Circle actually adds on top: paid membership, gated spaces, and the joining and leaving flow. The membership layer is small. The forum is not. Getting that ratio right is the whole reason this is achievable at all. ### Stack (fixed, do not substitute) - Community: Discourse, self-hosted with its official install · posts, threading, moderation, search, trust levels and spam handling, none of which you should write - Membership: A small Node 22 service alongside it · the only genuinely custom part: subscriptions in, group membership out - Billing: Stripe Checkout and the customer portal, driven by webhooks · hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build - Identity: Discourse as the single sign-on provider for your membership pages · one account, and the forum stays the source of truth for who someone is - Email: A transactional provider with a verified sending domain · a community whose notifications land in spam is a dead community ### Data model (create this before Phase 1) - Discourse owns users, posts, categories and groups. Your service owns only the commercial relationship. - members(id, discourse_user_id, email, stripe_customer_id, created_at) - subscriptions(id, member_id, stripe_subscription_id, plan, state, current_period_end, cancel_at_period_end) - grants(id, member_id, group_name, granted_at, revoked_at, reason) · an audit trail of every access change - webhook_events(id, stripe_event_id, type, processed_at) · the idempotency record, because Stripe redelivers - The rule that keeps this small: a subscription state change is the only thing that ever adds or removes someone from a group. ### Phase 1 · Discourse running properly Build: Install Discourse using its official supported method. Point a domain at it, get certificates, and configure the transactional email provider. Then send a real invitation to a real address and confirm it lands in an inbox rather than spam. Trap: Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. Done when: Discourse is reachable over HTTPS with a valid certificate. An invitation email arrives in an inbox and is not in spam. An account can be created, a post written and a reply notification received. Do not build yet: No spaces, no payments, no membership service. ### Phase 2 · Spaces as groups and private categories Build: Create a group per membership tier and a private category per space, with the category visible only to its groups. Verify by signing in as a member of neither group and confirming the space is absent rather than merely unreadable. Trap: A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. Done when: A user in no group cannot see the private category exists, including in search. Adding them to the group makes it appear without a re-login. Removing them makes it disappear. A logged-out visitor sees no trace of it. Do not build yet: No payments yet, so group membership is manual for now. ### Phase 3 · Subscriptions Build: A membership service with a Stripe Checkout flow per tier and a webhook endpoint that is the single source of truth for subscription state. Verify the signature, record processed event identifiers, and make every handler idempotent. Trap: Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. Done when: A test-mode subscription creates a member and a subscription row only after the webhook arrives. Replaying the same event leaves exactly one subscription and one grant. A webhook with a bad signature is rejected with a 400. Closing the browser immediately after paying still results in an active subscription. Do not build yet: No group granting yet, which is the next phase. ### Phase 4 · Access follows the subscription Build: On subscription activation, add the member to the tier's group through the Discourse admin API. On cancellation or payment failure, remove them at the end of the paid period rather than immediately. Write every grant and revocation to the audit table. Trap: Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. Done when: An activated subscription adds the member to the group within seconds and they can see the space. A cancellation with a period end in the future leaves access intact until that date, then removes it. A failed payment does not remove access while Stripe is still retrying. Every change appears in the audit table with a reason. Do not build yet: No member-facing account pages yet. ### Phase 5 · Single sign-on and self-service Build: Use Discourse as the identity provider for your membership pages, so a member signs in once. Give them a Stripe customer portal link to update cards, change tier or cancel. Cancellation must work without emailing you. Trap: Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. Done when: A member signs in to the membership pages without entering a second password. The customer portal opens for the correct customer and a card update is reflected. A member can cancel entirely through the portal, and the resulting webhook schedules their access removal. Do not build yet: Operations hardening is the last phase. ### Phase 6 · Backups, moderation and the restore drill Build: Back up Discourse and your membership database off the machine nightly. Restore both onto a clean server and record the date. Write the moderation policy and configure the platform's own tools to match it. Trap: Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. Done when: A restore onto a clean server reproduces the forum and the membership database, and a reconciliation pass confirms group membership matches active subscriptions. The date is recorded. A moderation policy is written and the platform settings match it. Do not build yet: Nothing after this. ### Out of scope (and why) - The member experience polish. Circle's interface is a considerable part of what people pay for, and Discourse looks and feels like a forum, because it is one. - Native mobile applications with push notifications, which members of paid communities expect. - Live events, courses and the bundled feature set that makes the platform a suite rather than a forum. - Anyone else being responsible for uptime, spam and abuse. Running a community is ongoing work regardless of the software. ### README must contain - That Discourse is the community and your service only manages the commercial relationship - That outbound email must be verified before anything else works - That the Stripe webhook is the only thing that grants or revokes access - That a failed payment does not remove access while retries are in progress, and when it does - The backup schedule for both databases and the date of the last restore and reconciliation ===== AGENTS.md ===== # Agent instructions · Circle indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Discourse, self-hosted with its official install, A small Node 22 service alongside it, Stripe Checkout and the customer portal, driven by webhooks, Discourse as the single sign-on provider for your membership pages, A transactional provider with a verified sending domain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ===== BUILD_PLAN.md ===== # Build plan · Circle Discourse self-hosted as the community itself, with private categories as spaces, a small membership service that takes Stripe subscriptions and grants or revokes group access as they start and lapse, single sign-on so members never manage a second password, and email notifications that actually arrive. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Discourse running properly A real community platform is up, on your domain, sending email that arrives. ### Steps 1. Install with the supported method Use the official installation. Unsupported variations are where the long support threads come from, and you will not get help with them. 2. Verify the sending domain Add the DNS records your provider requires and wait for verification. Nothing else in this build works until mail does. 3. Prove the loop end to end Invite a real address, sign up, post, and receive the notification. Each step failing has a different cause, so test them separately. ### Done when - [ ] Discourse serves over HTTPS with a valid certificate - [ ] An invitation email arrives in an inbox rather than spam - [ ] A reply notification is delivered - [ ] The admin account has two factor authentication enabled ### Watch out - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. ## Phase 2 · Spaces as groups and private categories Access is expressed once, as group membership, and every space respects it. ### Steps 1. Create one group per tier Groups are the only access primitive you will use. Everything downstream adds and removes group membership and nothing else. 2. Create private categories with visibility restricted Set both see and reply permissions to the groups. Visibility is the setting people forget. 3. Test as an outsider Log out, search for a phrase you posted in the private category, and confirm nothing is returned. ### Done when - [ ] A non-member cannot see the private category exists - [ ] A search by a non-member returns nothing from it - [ ] Adding to the group grants access without a re-login - [ ] Removing from the group revokes access immediately ### Watch out - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. ## Phase 3 · Subscriptions Someone can pay, and the webhook is the only thing that believes they did. ### Steps 1. Create Checkout sessions per tier Prices defined in Stripe, never passed from the client. Attach your own member identifier as metadata so the webhook can find them. Files: `src/checkout.ts` 2. Verify signatures and record event ids Signature verification with the endpoint secret, and a processed-events table so redelivery is a no-op. Files: `src/webhooks.ts` 3. Test with replay Use the Stripe command line tool to trigger and then replay an event. Assert the second delivery changes nothing. ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` ### Done when - [ ] A subscription is recorded only after the webhook - [ ] Replaying an event changes nothing the second time - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in an active subscription ### Watch out - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. ## Phase 4 · Access follows the subscription Paying grants access, lapsing removes it, and every change is recorded. ### Steps 1. Call the Discourse admin API Add and remove group members with an API key scoped as narrowly as the platform allows. This key is powerful, so treat it accordingly. Files: `src/discourse.ts` 2. Model the lapse correctly Access ends at the period end for a cancellation, and only at a terminal subscription state for payment failure. A scheduled job handles period ends. Files: `src/access.ts` 3. Record every change with a reason The audit table is what lets you answer why someone lost access, which will be asked. Files: `src/audit.ts` ### Done when - [ ] Activation grants group access within seconds - [ ] A cancellation retains access until the period end, then removes it - [ ] A failed payment during retries does not remove access - [ ] Every grant and revocation is recorded with a reason ### Watch out - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. ## Phase 5 · Single sign-on and self-service Members manage their own billing and never keep a second password. ### Steps 1. Use Discourse as the identity provider Its single sign-on flow signs members in to your pages. Your service stores an identifier, never a password. Files: `src/sso.ts` 2. Link the customer portal Generate a portal session for the signed-in member. Card updates, tier changes and cancellation are all handled there. Files: `src/portal.ts` 3. Confirm cancellation works end to end Cancel through the portal and watch the webhook schedule the removal. If this needs a human, it is not finished. ### Done when - [ ] Members sign in without a second password - [ ] The portal opens for the correct customer - [ ] A card update through the portal is reflected in the subscription - [ ] Cancellation through the portal schedules access removal with no manual step ### Watch out - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ## Not in this build - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. ## After v1, if you want it - An annual plan, which is a second Stripe price and a mapping entry - A trial period, handled entirely by Stripe rather than by your own logic - A monthly reconciliation report emailed to you, so drift is noticed without being looked for ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your Discourse installation. DISCOURSE_URL=https://community.example.com # Required · secret. Discourse admin API settings. Server only, never in a browser. DISCOURSE_API_KEY=replace-with-admin-api-key # Required. The account the API acts as. Use the system account rather than a person. DISCOURSE_API_USERNAME=system # Required · secret. Shared secret for the single sign-on flow. Must match the value configured in Discourse. DISCOURSE_SSO_SECRET=generate-a-long-random-value # Required · secret. Stripe dashboard. Stay in test mode until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Required · secret. From the registered endpoint, or printed by stripe listen during development. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your membership service's own database, separate from Discourse's. MEMBERSHIP_DATABASE_URL=postgres://user:pass@localhost:5432/membership # Required. Which Stripe price maps to which Discourse group. The whole access model in one line. TIER_GROUP_MAP=basic:members,pro:pro-members
You are building a lean indie version of Circle. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Circle · indie build Discourse self-hosted as the community itself, with private categories as spaces, a small membership service that takes Stripe subscriptions and grants or revokes group access as they start and lapse, single sign-on so members never manage a second password, and email notifications that actually arrive. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Community | Discourse, self-hosted with its official install | posts, threading, moderation, search, trust levels and spam handling, none of which you should write | | Membership | A small Node 22 service alongside it | the only genuinely custom part: subscriptions in, group membership out | | Billing | Stripe Checkout and the customer portal, driven by webhooks | hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build | | Identity | Discourse as the single sign-on provider for your membership pages | one account, and the forum stays the source of truth for who someone is | | Email | A transactional provider with a verified sending domain | a community whose notifications land in spam is a dead community | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server meeting Discourse's requirements** · typically 10 to 30 USD a month for a small community - Why: Discourse is not lightweight, and an undersized server produces a community that is slow in a way members notice immediately. - Get it: Check the current official requirements before choosing, then provision accordingly. Its own installation guide is the authority on the current figure. - Verify: The official installer completes and the site loads - [ ] **A domain with DNS you control** · roughly 10 to 15 USD a year - Why: Needed for the site, the certificate and the email sending domain records. - Get it: Any registrar. You will add A records and several email verification records. - Verify: The hostname resolves and the email records validate at your provider - [ ] **A transactional email provider with a verified sending domain** · free tiers typically cover a few thousand messages a month; a busy community will exceed that - Why: Discourse is unusable without it. Signup, invitations, password reset and every notification depend on outbound mail arriving. - Get it: Create an account, add the DNS records they specify for domain verification, and take the SMTP credentials. Verification propagation is the slow step, so start it first. - Verify: A test message from the server reaches an inbox and is not marked spam - [ ] **A Stripe account with test mode keys** · free in test mode; live payments cost per transaction plus a further fee on subscriptions in most regions - Why: Phase 3 needs the secret key and a webhook signing secret. Test mode does everything except move money. - Get it: Sign up at stripe.com, take the keys from Developers then API keys, and get the webhook secret from the endpoint you register or from the listen command. - Verify: stripe listen prints a signing secret and a triggered event reaches your endpoint - [ ] **A Discourse admin API key** · free - Why: The membership service adds and removes group members through it. It is a powerful credential and belongs only on the server. - Get it: Create it in the Discourse admin API settings, scoped as narrowly as the platform allows. - Verify: A group membership change through the API succeeds - [ ] **A moderation policy and someone to apply it** · free, except for the time it will take you every week afterwards - Why: A paid community with no policy makes one up in its first crisis, which is the worst time to write it. - Get it: Write what is not allowed, who decides and how someone appeals, before you invite anyone. One page is enough. - Verify: The policy is written and the platform settings reflect it ## Quick start ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. - polished member UX - mobile apps - payments - course/events integration - automations - migrations - community admin If one of those is essential to you, that is the reason to keep paying for Circle, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Circle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private paid community with spaces, roles, membership billing and notifications to replace Circle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a forum engine. Discourse already handles posts, threading, moderation, trust levels, search, spam and the thousand details that make a community usable, and it has done so for over a decade. Deploy it, then build the layer Circle actually adds on top: paid membership, gated spaces, and the joining and leaving flow. The membership layer is small. The forum is not. Getting that ratio right is the whole reason this is achievable at all. ### Stack (fixed, do not substitute) - Community: Discourse, self-hosted with its official install · posts, threading, moderation, search, trust levels and spam handling, none of which you should write - Membership: A small Node 22 service alongside it · the only genuinely custom part: subscriptions in, group membership out - Billing: Stripe Checkout and the customer portal, driven by webhooks · hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build - Identity: Discourse as the single sign-on provider for your membership pages · one account, and the forum stays the source of truth for who someone is - Email: A transactional provider with a verified sending domain · a community whose notifications land in spam is a dead community ### Data model (create this before Phase 1) - Discourse owns users, posts, categories and groups. Your service owns only the commercial relationship. - members(id, discourse_user_id, email, stripe_customer_id, created_at) - subscriptions(id, member_id, stripe_subscription_id, plan, state, current_period_end, cancel_at_period_end) - grants(id, member_id, group_name, granted_at, revoked_at, reason) · an audit trail of every access change - webhook_events(id, stripe_event_id, type, processed_at) · the idempotency record, because Stripe redelivers - The rule that keeps this small: a subscription state change is the only thing that ever adds or removes someone from a group. ### Phase 1 · Discourse running properly Build: Install Discourse using its official supported method. Point a domain at it, get certificates, and configure the transactional email provider. Then send a real invitation to a real address and confirm it lands in an inbox rather than spam. Trap: Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. Done when: Discourse is reachable over HTTPS with a valid certificate. An invitation email arrives in an inbox and is not in spam. An account can be created, a post written and a reply notification received. Do not build yet: No spaces, no payments, no membership service. ### Phase 2 · Spaces as groups and private categories Build: Create a group per membership tier and a private category per space, with the category visible only to its groups. Verify by signing in as a member of neither group and confirming the space is absent rather than merely unreadable. Trap: A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. Done when: A user in no group cannot see the private category exists, including in search. Adding them to the group makes it appear without a re-login. Removing them makes it disappear. A logged-out visitor sees no trace of it. Do not build yet: No payments yet, so group membership is manual for now. ### Phase 3 · Subscriptions Build: A membership service with a Stripe Checkout flow per tier and a webhook endpoint that is the single source of truth for subscription state. Verify the signature, record processed event identifiers, and make every handler idempotent. Trap: Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. Done when: A test-mode subscription creates a member and a subscription row only after the webhook arrives. Replaying the same event leaves exactly one subscription and one grant. A webhook with a bad signature is rejected with a 400. Closing the browser immediately after paying still results in an active subscription. Do not build yet: No group granting yet, which is the next phase. ### Phase 4 · Access follows the subscription Build: On subscription activation, add the member to the tier's group through the Discourse admin API. On cancellation or payment failure, remove them at the end of the paid period rather than immediately. Write every grant and revocation to the audit table. Trap: Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. Done when: An activated subscription adds the member to the group within seconds and they can see the space. A cancellation with a period end in the future leaves access intact until that date, then removes it. A failed payment does not remove access while Stripe is still retrying. Every change appears in the audit table with a reason. Do not build yet: No member-facing account pages yet. ### Phase 5 · Single sign-on and self-service Build: Use Discourse as the identity provider for your membership pages, so a member signs in once. Give them a Stripe customer portal link to update cards, change tier or cancel. Cancellation must work without emailing you. Trap: Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. Done when: A member signs in to the membership pages without entering a second password. The customer portal opens for the correct customer and a card update is reflected. A member can cancel entirely through the portal, and the resulting webhook schedules their access removal. Do not build yet: Operations hardening is the last phase. ### Phase 6 · Backups, moderation and the restore drill Build: Back up Discourse and your membership database off the machine nightly. Restore both onto a clean server and record the date. Write the moderation policy and configure the platform's own tools to match it. Trap: Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. Done when: A restore onto a clean server reproduces the forum and the membership database, and a reconciliation pass confirms group membership matches active subscriptions. The date is recorded. A moderation policy is written and the platform settings match it. Do not build yet: Nothing after this. ### Out of scope (and why) - The member experience polish. Circle's interface is a considerable part of what people pay for, and Discourse looks and feels like a forum, because it is one. - Native mobile applications with push notifications, which members of paid communities expect. - Live events, courses and the bundled feature set that makes the platform a suite rather than a forum. - Anyone else being responsible for uptime, spam and abuse. Running a community is ongoing work regardless of the software. ### README must contain - That Discourse is the community and your service only manages the commercial relationship - That outbound email must be verified before anything else works - That the Stripe webhook is the only thing that grants or revokes access - That a failed payment does not remove access while retries are in progress, and when it does - The backup schedule for both databases and the date of the last restore and reconciliation ===== AGENTS.md ===== # Agent instructions · Circle indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Discourse, self-hosted with its official install, A small Node 22 service alongside it, Stripe Checkout and the customer portal, driven by webhooks, Discourse as the single sign-on provider for your membership pages, A transactional provider with a verified sending domain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ===== BUILD_PLAN.md ===== # Build plan · Circle Discourse self-hosted as the community itself, with private categories as spaces, a small membership service that takes Stripe subscriptions and grants or revokes group access as they start and lapse, single sign-on so members never manage a second password, and email notifications that actually arrive. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Discourse running properly A real community platform is up, on your domain, sending email that arrives. ### Steps 1. Install with the supported method Use the official installation. Unsupported variations are where the long support threads come from, and you will not get help with them. 2. Verify the sending domain Add the DNS records your provider requires and wait for verification. Nothing else in this build works until mail does. 3. Prove the loop end to end Invite a real address, sign up, post, and receive the notification. Each step failing has a different cause, so test them separately. ### Done when - [ ] Discourse serves over HTTPS with a valid certificate - [ ] An invitation email arrives in an inbox rather than spam - [ ] A reply notification is delivered - [ ] The admin account has two factor authentication enabled ### Watch out - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. ## Phase 2 · Spaces as groups and private categories Access is expressed once, as group membership, and every space respects it. ### Steps 1. Create one group per tier Groups are the only access primitive you will use. Everything downstream adds and removes group membership and nothing else. 2. Create private categories with visibility restricted Set both see and reply permissions to the groups. Visibility is the setting people forget. 3. Test as an outsider Log out, search for a phrase you posted in the private category, and confirm nothing is returned. ### Done when - [ ] A non-member cannot see the private category exists - [ ] A search by a non-member returns nothing from it - [ ] Adding to the group grants access without a re-login - [ ] Removing from the group revokes access immediately ### Watch out - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. ## Phase 3 · Subscriptions Someone can pay, and the webhook is the only thing that believes they did. ### Steps 1. Create Checkout sessions per tier Prices defined in Stripe, never passed from the client. Attach your own member identifier as metadata so the webhook can find them. Files: `src/checkout.ts` 2. Verify signatures and record event ids Signature verification with the endpoint secret, and a processed-events table so redelivery is a no-op. Files: `src/webhooks.ts` 3. Test with replay Use the Stripe command line tool to trigger and then replay an event. Assert the second delivery changes nothing. ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` ### Done when - [ ] A subscription is recorded only after the webhook - [ ] Replaying an event changes nothing the second time - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in an active subscription ### Watch out - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. ## Phase 4 · Access follows the subscription Paying grants access, lapsing removes it, and every change is recorded. ### Steps 1. Call the Discourse admin API Add and remove group members with an API key scoped as narrowly as the platform allows. This key is powerful, so treat it accordingly. Files: `src/discourse.ts` 2. Model the lapse correctly Access ends at the period end for a cancellation, and only at a terminal subscription state for payment failure. A scheduled job handles period ends. Files: `src/access.ts` 3. Record every change with a reason The audit table is what lets you answer why someone lost access, which will be asked. Files: `src/audit.ts` ### Done when - [ ] Activation grants group access within seconds - [ ] A cancellation retains access until the period end, then removes it - [ ] A failed payment during retries does not remove access - [ ] Every grant and revocation is recorded with a reason ### Watch out - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. ## Phase 5 · Single sign-on and self-service Members manage their own billing and never keep a second password. ### Steps 1. Use Discourse as the identity provider Its single sign-on flow signs members in to your pages. Your service stores an identifier, never a password. Files: `src/sso.ts` 2. Link the customer portal Generate a portal session for the signed-in member. Card updates, tier changes and cancellation are all handled there. Files: `src/portal.ts` 3. Confirm cancellation works end to end Cancel through the portal and watch the webhook schedule the removal. If this needs a human, it is not finished. ### Done when - [ ] Members sign in without a second password - [ ] The portal opens for the correct customer - [ ] A card update through the portal is reflected in the subscription - [ ] Cancellation through the portal schedules access removal with no manual step ### Watch out - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ## Not in this build - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. ## After v1, if you want it - An annual plan, which is a second Stripe price and a mapping entry - A trial period, handled entirely by Stripe rather than by your own logic - A monthly reconciliation report emailed to you, so drift is noticed without being looked for ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your Discourse installation. DISCOURSE_URL=https://community.example.com # Required · secret. Discourse admin API settings. Server only, never in a browser. DISCOURSE_API_KEY=replace-with-admin-api-key # Required. The account the API acts as. Use the system account rather than a person. DISCOURSE_API_USERNAME=system # Required · secret. Shared secret for the single sign-on flow. Must match the value configured in Discourse. DISCOURSE_SSO_SECRET=generate-a-long-random-value # Required · secret. Stripe dashboard. Stay in test mode until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Required · secret. From the registered endpoint, or printed by stripe listen during development. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your membership service's own database, separate from Discourse's. MEMBERSHIP_DATABASE_URL=postgres://user:pass@localhost:5432/membership # Required. Which Stripe price maps to which Discourse group. The whole access model in one line. TIER_GROUP_MAP=basic:members,pro:pro-members
You are building a production product version of Circle. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Circle · product brief ## Problem A private forum/community is buildable, but Circle's value is member management, courses, events, payments, mobile apps, automations, AI, and migrations. ## Product outcome A paid private community where the forum is a mature platform rather than something you wrote, access is granted and revoked only by subscription state, members manage their own billing, and both databases have been restored together and reconciled. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - auth - database - forum/community app - payments - email/push notifications - hosting ## Explicit non-goals for v1 - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. - polished member UX - mobile apps - payments - course/events integration - automations - migrations - community admin ## Success criteria - A replayed Stripe webhook is proved not to double-grant - A non-member is confirmed unable to see a private category, including in search - Cancellation through the portal removes access at the period end with no manual step - Both databases have been restored together and reconciled with zero differences - The moderation policy is written and the platform settings match it ===== BRIEF.md ===== # Build brief · Circle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private paid community with spaces, roles, membership billing and notifications to replace Circle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a forum engine. Discourse already handles posts, threading, moderation, trust levels, search, spam and the thousand details that make a community usable, and it has done so for over a decade. Deploy it, then build the layer Circle actually adds on top: paid membership, gated spaces, and the joining and leaving flow. The membership layer is small. The forum is not. Getting that ratio right is the whole reason this is achievable at all. ### Stack (fixed, do not substitute) - Community: Discourse, self-hosted with its official install · posts, threading, moderation, search, trust levels and spam handling, none of which you should write - Membership: A small Node 22 service alongside it · the only genuinely custom part: subscriptions in, group membership out - Billing: Stripe Checkout and the customer portal, driven by webhooks · hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build - Identity: Discourse as the single sign-on provider for your membership pages · one account, and the forum stays the source of truth for who someone is - Email: A transactional provider with a verified sending domain · a community whose notifications land in spam is a dead community ### Data model (create this before Phase 1) - Discourse owns users, posts, categories and groups. Your service owns only the commercial relationship. - members(id, discourse_user_id, email, stripe_customer_id, created_at) - subscriptions(id, member_id, stripe_subscription_id, plan, state, current_period_end, cancel_at_period_end) - grants(id, member_id, group_name, granted_at, revoked_at, reason) · an audit trail of every access change - webhook_events(id, stripe_event_id, type, processed_at) · the idempotency record, because Stripe redelivers - The rule that keeps this small: a subscription state change is the only thing that ever adds or removes someone from a group. ### Phase 1 · Discourse running properly Build: Install Discourse using its official supported method. Point a domain at it, get certificates, and configure the transactional email provider. Then send a real invitation to a real address and confirm it lands in an inbox rather than spam. Trap: Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. Done when: Discourse is reachable over HTTPS with a valid certificate. An invitation email arrives in an inbox and is not in spam. An account can be created, a post written and a reply notification received. Do not build yet: No spaces, no payments, no membership service. ### Phase 2 · Spaces as groups and private categories Build: Create a group per membership tier and a private category per space, with the category visible only to its groups. Verify by signing in as a member of neither group and confirming the space is absent rather than merely unreadable. Trap: A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. Done when: A user in no group cannot see the private category exists, including in search. Adding them to the group makes it appear without a re-login. Removing them makes it disappear. A logged-out visitor sees no trace of it. Do not build yet: No payments yet, so group membership is manual for now. ### Phase 3 · Subscriptions Build: A membership service with a Stripe Checkout flow per tier and a webhook endpoint that is the single source of truth for subscription state. Verify the signature, record processed event identifiers, and make every handler idempotent. Trap: Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. Done when: A test-mode subscription creates a member and a subscription row only after the webhook arrives. Replaying the same event leaves exactly one subscription and one grant. A webhook with a bad signature is rejected with a 400. Closing the browser immediately after paying still results in an active subscription. Do not build yet: No group granting yet, which is the next phase. ### Phase 4 · Access follows the subscription Build: On subscription activation, add the member to the tier's group through the Discourse admin API. On cancellation or payment failure, remove them at the end of the paid period rather than immediately. Write every grant and revocation to the audit table. Trap: Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. Done when: An activated subscription adds the member to the group within seconds and they can see the space. A cancellation with a period end in the future leaves access intact until that date, then removes it. A failed payment does not remove access while Stripe is still retrying. Every change appears in the audit table with a reason. Do not build yet: No member-facing account pages yet. ### Phase 5 · Single sign-on and self-service Build: Use Discourse as the identity provider for your membership pages, so a member signs in once. Give them a Stripe customer portal link to update cards, change tier or cancel. Cancellation must work without emailing you. Trap: Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. Done when: A member signs in to the membership pages without entering a second password. The customer portal opens for the correct customer and a card update is reflected. A member can cancel entirely through the portal, and the resulting webhook schedules their access removal. Do not build yet: Operations hardening is the last phase. ### Phase 6 · Backups, moderation and the restore drill Build: Back up Discourse and your membership database off the machine nightly. Restore both onto a clean server and record the date. Write the moderation policy and configure the platform's own tools to match it. Trap: Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. Done when: A restore onto a clean server reproduces the forum and the membership database, and a reconciliation pass confirms group membership matches active subscriptions. The date is recorded. A moderation policy is written and the platform settings match it. Do not build yet: Nothing after this. ### Out of scope (and why) - The member experience polish. Circle's interface is a considerable part of what people pay for, and Discourse looks and feels like a forum, because it is one. - Native mobile applications with push notifications, which members of paid communities expect. - Live events, courses and the bundled feature set that makes the platform a suite rather than a forum. - Anyone else being responsible for uptime, spam and abuse. Running a community is ongoing work regardless of the software. ### README must contain - That Discourse is the community and your service only manages the commercial relationship - That outbound email must be verified before anything else works - That the Stripe webhook is the only thing that grants or revokes access - That a failed payment does not remove access while retries are in progress, and when it does - The backup schedule for both databases and the date of the last restore and reconciliation ===== ARCHITECTURE.md ===== # Architecture · Circle ## Stack | Part | Choice | Why | | --- | --- | --- | | Community | Discourse, self-hosted with its official install | posts, threading, moderation, search, trust levels and spam handling, none of which you should write | | Membership | A small Node 22 service alongside it | the only genuinely custom part: subscriptions in, group membership out | | Billing | Stripe Checkout and the customer portal, driven by webhooks | hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build | | Identity | Discourse as the single sign-on provider for your membership pages | one account, and the forum stays the source of truth for who someone is | | Email | A transactional provider with a verified sending domain | a community whose notifications land in spam is a dead community | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Community | Posts, threading, moderation, search and notifications. | Discourse. Replacing it means writing a forum, which is the thing rule zero exists to prevent. | | Membership | Subscriptions and the mapping from tier to group. | The only genuinely custom component, and deliberately small. Keep it that way. | | Billing | Checkout, the customer portal and webhooks. | Another processor changes the session call and the signature check. The idempotent webhook shape and the period-end logic survive. | | Identity | Who someone is. | Discourse is the provider so your service holds no credentials. Any change here should preserve that property. | | Audit | Why access changed and when. | Append-only and small. It is what lets you answer a member's question without guessing. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DISCOURSE_URL` · required · Your Discourse installation. - `DISCOURSE_API_KEY` · required, secret · Discourse admin API settings. Server only, never in a browser. - `DISCOURSE_API_USERNAME` · required · The account the API acts as. Use the system account rather than a person. - `DISCOURSE_SSO_SECRET` · required, secret · Shared secret for the single sign-on flow. Must match the value configured in Discourse. - `STRIPE_SECRET_KEY` · required, secret · Stripe dashboard. Stay in test mode until the restore drill has been done. - `STRIPE_WEBHOOK_SECRET` · required, secret · From the registered endpoint, or printed by stripe listen during development. - `MEMBERSHIP_DATABASE_URL` · required, secret · Your membership service's own database, separate from Discourse's. - `TIER_GROUP_MAP` · required · Which Stripe price maps to which Discourse group. The whole access model in one line. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Circle product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Discourse, self-hosted with its official install, A small Node 22 service alongside it, Stripe Checkout and the customer portal, driven by webhooks, Discourse as the single sign-on provider for your membership pages, A transactional provider with a verified sending domain. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. - Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. ===== MILESTONES.md ===== # Delivery milestones · Circle Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Discourse running properly A real community platform is up, on your domain, sending email that arrives. ### Steps 1. Install with the supported method Use the official installation. Unsupported variations are where the long support threads come from, and you will not get help with them. 2. Verify the sending domain Add the DNS records your provider requires and wait for verification. Nothing else in this build works until mail does. 3. Prove the loop end to end Invite a real address, sign up, post, and receive the notification. Each step failing has a different cause, so test them separately. ### Done when - [ ] Discourse serves over HTTPS with a valid certificate - [ ] An invitation email arrives in an inbox rather than spam - [ ] A reply notification is delivered - [ ] The admin account has two factor authentication enabled ### Watch out - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. ## M2 · Spaces as groups and private categories Access is expressed once, as group membership, and every space respects it. ### Steps 1. Create one group per tier Groups are the only access primitive you will use. Everything downstream adds and removes group membership and nothing else. 2. Create private categories with visibility restricted Set both see and reply permissions to the groups. Visibility is the setting people forget. 3. Test as an outsider Log out, search for a phrase you posted in the private category, and confirm nothing is returned. ### Done when - [ ] A non-member cannot see the private category exists - [ ] A search by a non-member returns nothing from it - [ ] Adding to the group grants access without a re-login - [ ] Removing from the group revokes access immediately ### Watch out - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. ## M3 · Subscriptions Someone can pay, and the webhook is the only thing that believes they did. ### Steps 1. Create Checkout sessions per tier Prices defined in Stripe, never passed from the client. Attach your own member identifier as metadata so the webhook can find them. Files: `src/checkout.ts` 2. Verify signatures and record event ids Signature verification with the endpoint secret, and a processed-events table so redelivery is a no-op. Files: `src/webhooks.ts` 3. Test with replay Use the Stripe command line tool to trigger and then replay an event. Assert the second delivery changes nothing. ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` ### Done when - [ ] A subscription is recorded only after the webhook - [ ] Replaying an event changes nothing the second time - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in an active subscription ### Watch out - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. ## M4 · Access follows the subscription Paying grants access, lapsing removes it, and every change is recorded. ### Steps 1. Call the Discourse admin API Add and remove group members with an API key scoped as narrowly as the platform allows. This key is powerful, so treat it accordingly. Files: `src/discourse.ts` 2. Model the lapse correctly Access ends at the period end for a cancellation, and only at a terminal subscription state for payment failure. A scheduled job handles period ends. Files: `src/access.ts` 3. Record every change with a reason The audit table is what lets you answer why someone lost access, which will be asked. Files: `src/audit.ts` ### Done when - [ ] Activation grants group access within seconds - [ ] A cancellation retains access until the period end, then removes it - [ ] A failed payment during retries does not remove access - [ ] Every grant and revocation is recorded with a reason ### Watch out - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. ## M5 · Single sign-on and self-service Members manage their own billing and never keep a second password. ### Steps 1. Use Discourse as the identity provider Its single sign-on flow signs members in to your pages. Your service stores an identifier, never a password. Files: `src/sso.ts` 2. Link the customer portal Generate a portal session for the signed-in member. Card updates, tier changes and cancellation are all handled there. Files: `src/portal.ts` 3. Confirm cancellation works end to end Cancel through the portal and watch the webhook schedule the removal. If this needs a human, it is not finished. ### Done when - [ ] Members sign in without a second password - [ ] The portal opens for the correct customer - [ ] A card update through the portal is reflected in the subscription - [ ] Cancellation through the portal schedules access removal with no manual step ### Watch out - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ## M6 · Backups, moderation and the restore drill (production only) The community survives a server loss, and someone is responsible for what is posted. ### Steps 1. Back up both databases off the machine Discourse's own backup plus your membership database, dated, several generations, off the server. Files: `scripts/backup.sh` 2. Restore both and reconcile On a clean server, restore both, then run a reconciliation that compares group membership against active subscriptions and reports differences. Files: `scripts/reconcile.ts`, `README.md` 3. Write the moderation policy What is not allowed, who decides, and how someone appeals. Then set the platform's trust levels and flags to match. A community without this has one anyway, made up under pressure. Files: `docs/moderation.md` ### Done when - [ ] Both databases restore onto a clean server - [ ] Reconciliation reports no difference between groups and active subscriptions - [ ] The restore date is recorded - [ ] A written moderation policy exists and the platform settings match it ### Watch out - Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. ===== OPERATIONS.md ===== # Operations · Circle ## Backup Discourse's own backup plus your membership database, both nightly, both dated, both off the server. They must be restored as a pair or group membership will not match who is paying. ## Restore Restore both onto a clean server, then run the reconciliation that compares groups against active subscriptions and reports differences. Record the date. The reconciliation is what makes a paired restore trustworthy. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on the forum, email delivery and bounce rates, webhook processing failures, and the reconciliation difference count. A non-zero difference is the signal that access and billing have drifted. ## Incident checklist For a webhook outage, Stripe retries for a period, so the priority is fixing the endpoint rather than replaying by hand. For an email delivery failure, check the sending domain records first. For a disputed access removal, the audit table is the record of what happened and why. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A replayed Stripe webhook is proved not to double-grant - [ ] A non-member is confirmed unable to see a private category, including in search - [ ] Cancellation through the portal removes access at the period end with no manual step - [ ] Both databases have been restored together and reconciled with zero differences - [ ] The moderation policy is written and the platform settings match it ## Launch constraint Do not market omitted Circle capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your Discourse installation. DISCOURSE_URL=https://community.example.com # Required · secret. Discourse admin API settings. Server only, never in a browser. DISCOURSE_API_KEY=replace-with-admin-api-key # Required. The account the API acts as. Use the system account rather than a person. DISCOURSE_API_USERNAME=system # Required · secret. Shared secret for the single sign-on flow. Must match the value configured in Discourse. DISCOURSE_SSO_SECRET=generate-a-long-random-value # Required · secret. Stripe dashboard. Stay in test mode until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Required · secret. From the registered endpoint, or printed by stripe listen during development. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your membership service's own database, separate from Discourse's. MEMBERSHIP_DATABASE_URL=postgres://user:pass@localhost:5432/membership # Required. Which Stripe price maps to which Discourse group. The whole access model in one line. TIER_GROUP_MAP=basic:members,pro:pro-members
# Circle · indie build Discourse self-hosted as the community itself, with private categories as spaces, a small membership service that takes Stripe subscriptions and grants or revokes group access as they start and lapse, single sign-on so members never manage a second password, and email notifications that actually arrive. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Community | Discourse, self-hosted with its official install | posts, threading, moderation, search, trust levels and spam handling, none of which you should write | | Membership | A small Node 22 service alongside it | the only genuinely custom part: subscriptions in, group membership out | | Billing | Stripe Checkout and the customer portal, driven by webhooks | hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build | | Identity | Discourse as the single sign-on provider for your membership pages | one account, and the forum stays the source of truth for who someone is | | Email | A transactional provider with a verified sending domain | a community whose notifications land in spam is a dead community | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server meeting Discourse's requirements** · typically 10 to 30 USD a month for a small community - Why: Discourse is not lightweight, and an undersized server produces a community that is slow in a way members notice immediately. - Get it: Check the current official requirements before choosing, then provision accordingly. Its own installation guide is the authority on the current figure. - Verify: The official installer completes and the site loads - [ ] **A domain with DNS you control** · roughly 10 to 15 USD a year - Why: Needed for the site, the certificate and the email sending domain records. - Get it: Any registrar. You will add A records and several email verification records. - Verify: The hostname resolves and the email records validate at your provider - [ ] **A transactional email provider with a verified sending domain** · free tiers typically cover a few thousand messages a month; a busy community will exceed that - Why: Discourse is unusable without it. Signup, invitations, password reset and every notification depend on outbound mail arriving. - Get it: Create an account, add the DNS records they specify for domain verification, and take the SMTP credentials. Verification propagation is the slow step, so start it first. - Verify: A test message from the server reaches an inbox and is not marked spam - [ ] **A Stripe account with test mode keys** · free in test mode; live payments cost per transaction plus a further fee on subscriptions in most regions - Why: Phase 3 needs the secret key and a webhook signing secret. Test mode does everything except move money. - Get it: Sign up at stripe.com, take the keys from Developers then API keys, and get the webhook secret from the endpoint you register or from the listen command. - Verify: stripe listen prints a signing secret and a triggered event reaches your endpoint - [ ] **A Discourse admin API key** · free - Why: The membership service adds and removes group members through it. It is a powerful credential and belongs only on the server. - Get it: Create it in the Discourse admin API settings, scoped as narrowly as the platform allows. - Verify: A group membership change through the API succeeds - [ ] **A moderation policy and someone to apply it** · free, except for the time it will take you every week afterwards - Why: A paid community with no policy makes one up in its first crisis, which is the worst time to write it. - Get it: Write what is not allowed, who decides and how someone appeals, before you invite anyone. One page is enough. - Verify: The policy is written and the platform settings reflect it ## Quick start ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. - polished member UX - mobile apps - payments - course/events integration - automations - migrations - community admin If one of those is essential to you, that is the reason to keep paying for Circle, and the README should say so rather than pretend.
# Build brief · Circle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private paid community with spaces, roles, membership billing and notifications to replace Circle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a forum engine. Discourse already handles posts, threading, moderation, trust levels, search, spam and the thousand details that make a community usable, and it has done so for over a decade. Deploy it, then build the layer Circle actually adds on top: paid membership, gated spaces, and the joining and leaving flow. The membership layer is small. The forum is not. Getting that ratio right is the whole reason this is achievable at all. ### Stack (fixed, do not substitute) - Community: Discourse, self-hosted with its official install · posts, threading, moderation, search, trust levels and spam handling, none of which you should write - Membership: A small Node 22 service alongside it · the only genuinely custom part: subscriptions in, group membership out - Billing: Stripe Checkout and the customer portal, driven by webhooks · hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build - Identity: Discourse as the single sign-on provider for your membership pages · one account, and the forum stays the source of truth for who someone is - Email: A transactional provider with a verified sending domain · a community whose notifications land in spam is a dead community ### Data model (create this before Phase 1) - Discourse owns users, posts, categories and groups. Your service owns only the commercial relationship. - members(id, discourse_user_id, email, stripe_customer_id, created_at) - subscriptions(id, member_id, stripe_subscription_id, plan, state, current_period_end, cancel_at_period_end) - grants(id, member_id, group_name, granted_at, revoked_at, reason) · an audit trail of every access change - webhook_events(id, stripe_event_id, type, processed_at) · the idempotency record, because Stripe redelivers - The rule that keeps this small: a subscription state change is the only thing that ever adds or removes someone from a group. ### Phase 1 · Discourse running properly Build: Install Discourse using its official supported method. Point a domain at it, get certificates, and configure the transactional email provider. Then send a real invitation to a real address and confirm it lands in an inbox rather than spam. Trap: Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. Done when: Discourse is reachable over HTTPS with a valid certificate. An invitation email arrives in an inbox and is not in spam. An account can be created, a post written and a reply notification received. Do not build yet: No spaces, no payments, no membership service. ### Phase 2 · Spaces as groups and private categories Build: Create a group per membership tier and a private category per space, with the category visible only to its groups. Verify by signing in as a member of neither group and confirming the space is absent rather than merely unreadable. Trap: A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. Done when: A user in no group cannot see the private category exists, including in search. Adding them to the group makes it appear without a re-login. Removing them makes it disappear. A logged-out visitor sees no trace of it. Do not build yet: No payments yet, so group membership is manual for now. ### Phase 3 · Subscriptions Build: A membership service with a Stripe Checkout flow per tier and a webhook endpoint that is the single source of truth for subscription state. Verify the signature, record processed event identifiers, and make every handler idempotent. Trap: Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. Done when: A test-mode subscription creates a member and a subscription row only after the webhook arrives. Replaying the same event leaves exactly one subscription and one grant. A webhook with a bad signature is rejected with a 400. Closing the browser immediately after paying still results in an active subscription. Do not build yet: No group granting yet, which is the next phase. ### Phase 4 · Access follows the subscription Build: On subscription activation, add the member to the tier's group through the Discourse admin API. On cancellation or payment failure, remove them at the end of the paid period rather than immediately. Write every grant and revocation to the audit table. Trap: Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. Done when: An activated subscription adds the member to the group within seconds and they can see the space. A cancellation with a period end in the future leaves access intact until that date, then removes it. A failed payment does not remove access while Stripe is still retrying. Every change appears in the audit table with a reason. Do not build yet: No member-facing account pages yet. ### Phase 5 · Single sign-on and self-service Build: Use Discourse as the identity provider for your membership pages, so a member signs in once. Give them a Stripe customer portal link to update cards, change tier or cancel. Cancellation must work without emailing you. Trap: Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. Done when: A member signs in to the membership pages without entering a second password. The customer portal opens for the correct customer and a card update is reflected. A member can cancel entirely through the portal, and the resulting webhook schedules their access removal. Do not build yet: Operations hardening is the last phase. ### Phase 6 · Backups, moderation and the restore drill Build: Back up Discourse and your membership database off the machine nightly. Restore both onto a clean server and record the date. Write the moderation policy and configure the platform's own tools to match it. Trap: Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. Done when: A restore onto a clean server reproduces the forum and the membership database, and a reconciliation pass confirms group membership matches active subscriptions. The date is recorded. A moderation policy is written and the platform settings match it. Do not build yet: Nothing after this. ### Out of scope (and why) - The member experience polish. Circle's interface is a considerable part of what people pay for, and Discourse looks and feels like a forum, because it is one. - Native mobile applications with push notifications, which members of paid communities expect. - Live events, courses and the bundled feature set that makes the platform a suite rather than a forum. - Anyone else being responsible for uptime, spam and abuse. Running a community is ongoing work regardless of the software. ### README must contain - That Discourse is the community and your service only manages the commercial relationship - That outbound email must be verified before anything else works - That the Stripe webhook is the only thing that grants or revokes access - That a failed payment does not remove access while retries are in progress, and when it does - The backup schedule for both databases and the date of the last restore and reconciliation
# Agent instructions · Circle indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Discourse, self-hosted with its official install, A small Node 22 service alongside it, Stripe Checkout and the customer portal, driven by webhooks, Discourse as the single sign-on provider for your membership pages, A transactional provider with a verified sending domain. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all.
# Build plan · Circle Discourse self-hosted as the community itself, with private categories as spaces, a small membership service that takes Stripe subscriptions and grants or revokes group access as they start and lapse, single sign-on so members never manage a second password, and email notifications that actually arrive. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Discourse running properly A real community platform is up, on your domain, sending email that arrives. ### Steps 1. Install with the supported method Use the official installation. Unsupported variations are where the long support threads come from, and you will not get help with them. 2. Verify the sending domain Add the DNS records your provider requires and wait for verification. Nothing else in this build works until mail does. 3. Prove the loop end to end Invite a real address, sign up, post, and receive the notification. Each step failing has a different cause, so test them separately. ### Done when - [ ] Discourse serves over HTTPS with a valid certificate - [ ] An invitation email arrives in an inbox rather than spam - [ ] A reply notification is delivered - [ ] The admin account has two factor authentication enabled ### Watch out - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. ## Phase 2 · Spaces as groups and private categories Access is expressed once, as group membership, and every space respects it. ### Steps 1. Create one group per tier Groups are the only access primitive you will use. Everything downstream adds and removes group membership and nothing else. 2. Create private categories with visibility restricted Set both see and reply permissions to the groups. Visibility is the setting people forget. 3. Test as an outsider Log out, search for a phrase you posted in the private category, and confirm nothing is returned. ### Done when - [ ] A non-member cannot see the private category exists - [ ] A search by a non-member returns nothing from it - [ ] Adding to the group grants access without a re-login - [ ] Removing from the group revokes access immediately ### Watch out - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. ## Phase 3 · Subscriptions Someone can pay, and the webhook is the only thing that believes they did. ### Steps 1. Create Checkout sessions per tier Prices defined in Stripe, never passed from the client. Attach your own member identifier as metadata so the webhook can find them. Files: `src/checkout.ts` 2. Verify signatures and record event ids Signature verification with the endpoint secret, and a processed-events table so redelivery is a no-op. Files: `src/webhooks.ts` 3. Test with replay Use the Stripe command line tool to trigger and then replay an event. Assert the second delivery changes nothing. ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` ### Done when - [ ] A subscription is recorded only after the webhook - [ ] Replaying an event changes nothing the second time - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in an active subscription ### Watch out - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. ## Phase 4 · Access follows the subscription Paying grants access, lapsing removes it, and every change is recorded. ### Steps 1. Call the Discourse admin API Add and remove group members with an API key scoped as narrowly as the platform allows. This key is powerful, so treat it accordingly. Files: `src/discourse.ts` 2. Model the lapse correctly Access ends at the period end for a cancellation, and only at a terminal subscription state for payment failure. A scheduled job handles period ends. Files: `src/access.ts` 3. Record every change with a reason The audit table is what lets you answer why someone lost access, which will be asked. Files: `src/audit.ts` ### Done when - [ ] Activation grants group access within seconds - [ ] A cancellation retains access until the period end, then removes it - [ ] A failed payment during retries does not remove access - [ ] Every grant and revocation is recorded with a reason ### Watch out - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. ## Phase 5 · Single sign-on and self-service Members manage their own billing and never keep a second password. ### Steps 1. Use Discourse as the identity provider Its single sign-on flow signs members in to your pages. Your service stores an identifier, never a password. Files: `src/sso.ts` 2. Link the customer portal Generate a portal session for the signed-in member. Card updates, tier changes and cancellation are all handled there. Files: `src/portal.ts` 3. Confirm cancellation works end to end Cancel through the portal and watch the webhook schedule the removal. If this needs a human, it is not finished. ### Done when - [ ] Members sign in without a second password - [ ] The portal opens for the correct customer - [ ] A card update through the portal is reflected in the subscription - [ ] Cancellation through the portal schedules access removal with no manual step ### Watch out - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ## Not in this build - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. ## After v1, if you want it - An annual plan, which is a second Stripe price and a mapping entry - A trial period, handled entirely by Stripe rather than by your own logic - A monthly reconciliation report emailed to you, so drift is noticed without being looked for
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your Discourse installation. DISCOURSE_URL=https://community.example.com # Required · secret. Discourse admin API settings. Server only, never in a browser. DISCOURSE_API_KEY=replace-with-admin-api-key # Required. The account the API acts as. Use the system account rather than a person. DISCOURSE_API_USERNAME=system # Required · secret. Shared secret for the single sign-on flow. Must match the value configured in Discourse. DISCOURSE_SSO_SECRET=generate-a-long-random-value # Required · secret. Stripe dashboard. Stay in test mode until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Required · secret. From the registered endpoint, or printed by stripe listen during development. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your membership service's own database, separate from Discourse's. MEMBERSHIP_DATABASE_URL=postgres://user:pass@localhost:5432/membership # Required. Which Stripe price maps to which Discourse group. The whole access model in one line. TIER_GROUP_MAP=basic:members,pro:pro-members
# Circle · product brief ## Problem A private forum/community is buildable, but Circle's value is member management, courses, events, payments, mobile apps, automations, AI, and migrations. ## Product outcome A paid private community where the forum is a mature platform rather than something you wrote, access is granted and revoked only by subscription state, members manage their own billing, and both databases have been restored together and reconciled. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - auth - database - forum/community app - payments - email/push notifications - hosting ## Explicit non-goals for v1 - The polished member experience, which is a large part of what the subscription buys. - Native mobile applications with push notifications. - Live events, courses and the wider bundled feature set. - Anyone else carrying uptime, spam and abuse. - polished member UX - mobile apps - payments - course/events integration - automations - migrations - community admin ## Success criteria - A replayed Stripe webhook is proved not to double-grant - A non-member is confirmed unable to see a private category, including in search - Cancellation through the portal removes access at the period end with no manual step - Both databases have been restored together and reconciled with zero differences - The moderation policy is written and the platform settings match it
# Build brief · Circle The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private paid community with spaces, roles, membership billing and notifications to replace Circle. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a forum engine. Discourse already handles posts, threading, moderation, trust levels, search, spam and the thousand details that make a community usable, and it has done so for over a decade. Deploy it, then build the layer Circle actually adds on top: paid membership, gated spaces, and the joining and leaving flow. The membership layer is small. The forum is not. Getting that ratio right is the whole reason this is achievable at all. ### Stack (fixed, do not substitute) - Community: Discourse, self-hosted with its official install · posts, threading, moderation, search, trust levels and spam handling, none of which you should write - Membership: A small Node 22 service alongside it · the only genuinely custom part: subscriptions in, group membership out - Billing: Stripe Checkout and the customer portal, driven by webhooks · hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build - Identity: Discourse as the single sign-on provider for your membership pages · one account, and the forum stays the source of truth for who someone is - Email: A transactional provider with a verified sending domain · a community whose notifications land in spam is a dead community ### Data model (create this before Phase 1) - Discourse owns users, posts, categories and groups. Your service owns only the commercial relationship. - members(id, discourse_user_id, email, stripe_customer_id, created_at) - subscriptions(id, member_id, stripe_subscription_id, plan, state, current_period_end, cancel_at_period_end) - grants(id, member_id, group_name, granted_at, revoked_at, reason) · an audit trail of every access change - webhook_events(id, stripe_event_id, type, processed_at) · the idempotency record, because Stripe redelivers - The rule that keeps this small: a subscription state change is the only thing that ever adds or removes someone from a group. ### Phase 1 · Discourse running properly Build: Install Discourse using its official supported method. Point a domain at it, get certificates, and configure the transactional email provider. Then send a real invitation to a real address and confirm it lands in an inbox rather than spam. Trap: Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. Done when: Discourse is reachable over HTTPS with a valid certificate. An invitation email arrives in an inbox and is not in spam. An account can be created, a post written and a reply notification received. Do not build yet: No spaces, no payments, no membership service. ### Phase 2 · Spaces as groups and private categories Build: Create a group per membership tier and a private category per space, with the category visible only to its groups. Verify by signing in as a member of neither group and confirming the space is absent rather than merely unreadable. Trap: A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. Done when: A user in no group cannot see the private category exists, including in search. Adding them to the group makes it appear without a re-login. Removing them makes it disappear. A logged-out visitor sees no trace of it. Do not build yet: No payments yet, so group membership is manual for now. ### Phase 3 · Subscriptions Build: A membership service with a Stripe Checkout flow per tier and a webhook endpoint that is the single source of truth for subscription state. Verify the signature, record processed event identifiers, and make every handler idempotent. Trap: Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. Done when: A test-mode subscription creates a member and a subscription row only after the webhook arrives. Replaying the same event leaves exactly one subscription and one grant. A webhook with a bad signature is rejected with a 400. Closing the browser immediately after paying still results in an active subscription. Do not build yet: No group granting yet, which is the next phase. ### Phase 4 · Access follows the subscription Build: On subscription activation, add the member to the tier's group through the Discourse admin API. On cancellation or payment failure, remove them at the end of the paid period rather than immediately. Write every grant and revocation to the audit table. Trap: Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. Done when: An activated subscription adds the member to the group within seconds and they can see the space. A cancellation with a period end in the future leaves access intact until that date, then removes it. A failed payment does not remove access while Stripe is still retrying. Every change appears in the audit table with a reason. Do not build yet: No member-facing account pages yet. ### Phase 5 · Single sign-on and self-service Build: Use Discourse as the identity provider for your membership pages, so a member signs in once. Give them a Stripe customer portal link to update cards, change tier or cancel. Cancellation must work without emailing you. Trap: Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. Done when: A member signs in to the membership pages without entering a second password. The customer portal opens for the correct customer and a card update is reflected. A member can cancel entirely through the portal, and the resulting webhook schedules their access removal. Do not build yet: Operations hardening is the last phase. ### Phase 6 · Backups, moderation and the restore drill Build: Back up Discourse and your membership database off the machine nightly. Restore both onto a clean server and record the date. Write the moderation policy and configure the platform's own tools to match it. Trap: Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards. Done when: A restore onto a clean server reproduces the forum and the membership database, and a reconciliation pass confirms group membership matches active subscriptions. The date is recorded. A moderation policy is written and the platform settings match it. Do not build yet: Nothing after this. ### Out of scope (and why) - The member experience polish. Circle's interface is a considerable part of what people pay for, and Discourse looks and feels like a forum, because it is one. - Native mobile applications with push notifications, which members of paid communities expect. - Live events, courses and the bundled feature set that makes the platform a suite rather than a forum. - Anyone else being responsible for uptime, spam and abuse. Running a community is ongoing work regardless of the software. ### README must contain - That Discourse is the community and your service only manages the commercial relationship - That outbound email must be verified before anything else works - That the Stripe webhook is the only thing that grants or revokes access - That a failed payment does not remove access while retries are in progress, and when it does - The backup schedule for both databases and the date of the last restore and reconciliation
# Architecture · Circle ## Stack | Part | Choice | Why | | --- | --- | --- | | Community | Discourse, self-hosted with its official install | posts, threading, moderation, search, trust levels and spam handling, none of which you should write | | Membership | A small Node 22 service alongside it | the only genuinely custom part: subscriptions in, group membership out | | Billing | Stripe Checkout and the customer portal, driven by webhooks | hosted checkout and hosted subscription management, so cancellation and card updates are not yours to build | | Identity | Discourse as the single sign-on provider for your membership pages | one account, and the forum stays the source of truth for who someone is | | Email | A transactional provider with a verified sending domain | a community whose notifications land in spam is a dead community | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Community | Posts, threading, moderation, search and notifications. | Discourse. Replacing it means writing a forum, which is the thing rule zero exists to prevent. | | Membership | Subscriptions and the mapping from tier to group. | The only genuinely custom component, and deliberately small. Keep it that way. | | Billing | Checkout, the customer portal and webhooks. | Another processor changes the session call and the signature check. The idempotent webhook shape and the period-end logic survive. | | Identity | Who someone is. | Discourse is the provider so your service holds no credentials. Any change here should preserve that property. | | Audit | Why access changed and when. | Append-only and small. It is what lets you answer a member's question without guessing. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DISCOURSE_URL` · required · Your Discourse installation. - `DISCOURSE_API_KEY` · required, secret · Discourse admin API settings. Server only, never in a browser. - `DISCOURSE_API_USERNAME` · required · The account the API acts as. Use the system account rather than a person. - `DISCOURSE_SSO_SECRET` · required, secret · Shared secret for the single sign-on flow. Must match the value configured in Discourse. - `STRIPE_SECRET_KEY` · required, secret · Stripe dashboard. Stay in test mode until the restore drill has been done. - `STRIPE_WEBHOOK_SECRET` · required, secret · From the registered endpoint, or printed by stripe listen during development. - `MEMBERSHIP_DATABASE_URL` · required, secret · Your membership service's own database, separate from Discourse's. - `TIER_GROUP_MAP` · required · Which Stripe price maps to which Discourse group. The whole access model in one line. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Circle product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Discourse, self-hosted with its official install, A small Node 22 service alongside it, Stripe Checkout and the customer portal, driven by webhooks, Discourse as the single sign-on provider for your membership pages, A transactional provider with a verified sending domain. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. - Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards.
# Delivery milestones · Circle Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Discourse running properly A real community platform is up, on your domain, sending email that arrives. ### Steps 1. Install with the supported method Use the official installation. Unsupported variations are where the long support threads come from, and you will not get help with them. 2. Verify the sending domain Add the DNS records your provider requires and wait for verification. Nothing else in this build works until mail does. 3. Prove the loop end to end Invite a real address, sign up, post, and receive the notification. Each step failing has a different cause, so test them separately. ### Done when - [ ] Discourse serves over HTTPS with a valid certificate - [ ] An invitation email arrives in an inbox rather than spam - [ ] A reply notification is delivered - [ ] The admin account has two factor authentication enabled ### Watch out - Discourse is effectively unusable without working outbound email, because signup, invitations, password reset and notifications all depend on it. Verify the sending domain with the DNS records your provider specifies before you configure anything else, because propagation takes time and everything else waits on it. ## M2 · Spaces as groups and private categories Access is expressed once, as group membership, and every space respects it. ### Steps 1. Create one group per tier Groups are the only access primitive you will use. Everything downstream adds and removes group membership and nothing else. 2. Create private categories with visibility restricted Set both see and reply permissions to the groups. Visibility is the setting people forget. 3. Test as an outsider Log out, search for a phrase you posted in the private category, and confirm nothing is returned. ### Done when - [ ] A non-member cannot see the private category exists - [ ] A search by a non-member returns nothing from it - [ ] Adding to the group grants access without a re-login - [ ] Removing from the group revokes access immediately ### Watch out - A category that is visible but not readable tells non-members exactly what they are missing and leaks topic titles in search and in digest emails. Set visibility as well as read permission, and check what a logged-out visitor sees rather than assuming. ## M3 · Subscriptions Someone can pay, and the webhook is the only thing that believes they did. ### Steps 1. Create Checkout sessions per tier Prices defined in Stripe, never passed from the client. Attach your own member identifier as metadata so the webhook can find them. Files: `src/checkout.ts` 2. Verify signatures and record event ids Signature verification with the endpoint secret, and a processed-events table so redelivery is a no-op. Files: `src/webhooks.ts` 3. Test with replay Use the Stripe command line tool to trigger and then replay an event. Assert the second delivery changes nothing. ```sh stripe listen --forward-to localhost:3000/webhooks/stripe stripe trigger customer.subscription.created ``` ### Done when - [ ] A subscription is recorded only after the webhook - [ ] Replaying an event changes nothing the second time - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in an active subscription ### Watch out - Granting access on the success redirect is the classic mistake and it is worse here than usual, because the redirect can be replayed and it hands out community access rather than a download. The webhook is the truth; the redirect is a thank-you page. ## M4 · Access follows the subscription Paying grants access, lapsing removes it, and every change is recorded. ### Steps 1. Call the Discourse admin API Add and remove group members with an API key scoped as narrowly as the platform allows. This key is powerful, so treat it accordingly. Files: `src/discourse.ts` 2. Model the lapse correctly Access ends at the period end for a cancellation, and only at a terminal subscription state for payment failure. A scheduled job handles period ends. Files: `src/access.ts` 3. Record every change with a reason The audit table is what lets you answer why someone lost access, which will be asked. Files: `src/audit.ts` ### Done when - [ ] Activation grants group access within seconds - [ ] A cancellation retains access until the period end, then removes it - [ ] A failed payment during retries does not remove access - [ ] Every grant and revocation is recorded with a reason ### Watch out - Removing access the moment a card fails punishes people for an expired card rather than for leaving, and Stripe will retry that payment for days. Act on the subscription reaching a terminal state or the period ending, not on the first failed invoice. ## M5 · Single sign-on and self-service Members manage their own billing and never keep a second password. ### Steps 1. Use Discourse as the identity provider Its single sign-on flow signs members in to your pages. Your service stores an identifier, never a password. Files: `src/sso.ts` 2. Link the customer portal Generate a portal session for the signed-in member. Card updates, tier changes and cancellation are all handled there. Files: `src/portal.ts` 3. Confirm cancellation works end to end Cancel through the portal and watch the webhook schedule the removal. If this needs a human, it is not finished. ### Done when - [ ] Members sign in without a second password - [ ] The portal opens for the correct customer - [ ] A card update through the portal is reflected in the subscription - [ ] Cancellation through the portal schedules access removal with no manual step ### Watch out - Building your own account and password system beside Discourse gives every member two identities that drift apart, and gives you a password database you did not need. Let the forum be the identity provider and hold no credentials at all. ## M6 · Backups, moderation and the restore drill (production only) The community survives a server loss, and someone is responsible for what is posted. ### Steps 1. Back up both databases off the machine Discourse's own backup plus your membership database, dated, several generations, off the server. Files: `scripts/backup.sh` 2. Restore both and reconcile On a clean server, restore both, then run a reconciliation that compares group membership against active subscriptions and reports differences. Files: `scripts/reconcile.ts`, `README.md` 3. Write the moderation policy What is not allowed, who decides, and how someone appeals. Then set the platform's trust levels and flags to match. A community without this has one anyway, made up under pressure. Files: `docs/moderation.md` ### Done when - [ ] Both databases restore onto a clean server - [ ] Reconciliation reports no difference between groups and active subscriptions - [ ] The restore date is recorded - [ ] A written moderation policy exists and the platform settings match it ### Watch out - Restoring the forum without the membership database leaves groups populated from a backup that no longer matches who is paying, so lapsed members keep access and new ones have none. Restore both together and reconcile group membership against subscriptions afterwards.
# Operations · Circle ## Backup Discourse's own backup plus your membership database, both nightly, both dated, both off the server. They must be restored as a pair or group membership will not match who is paying. ## Restore Restore both onto a clean server, then run the reconciliation that compares groups against active subscriptions and reports differences. Record the date. The reconciliation is what makes a paired restore trustworthy. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on the forum, email delivery and bounce rates, webhook processing failures, and the reconciliation difference count. A non-zero difference is the signal that access and billing have drifted. ## Incident checklist For a webhook outage, Stripe retries for a period, so the priority is fixing the endpoint rather than replaying by hand. For an email delivery failure, check the sending domain records first. For a disputed access removal, the audit table is the record of what happened and why. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A replayed Stripe webhook is proved not to double-grant - [ ] A non-member is confirmed unable to see a private category, including in search - [ ] Cancellation through the portal removes access at the period end with no manual step - [ ] Both databases have been restored together and reconciled with zero differences - [ ] The moderation policy is written and the platform settings match it ## Launch constraint Do not market omitted Circle capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your Discourse installation. DISCOURSE_URL=https://community.example.com # Required · secret. Discourse admin API settings. Server only, never in a browser. DISCOURSE_API_KEY=replace-with-admin-api-key # Required. The account the API acts as. Use the system account rather than a person. DISCOURSE_API_USERNAME=system # Required · secret. Shared secret for the single sign-on flow. Must match the value configured in Discourse. DISCOURSE_SSO_SECRET=generate-a-long-random-value # Required · secret. Stripe dashboard. Stay in test mode until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Required · secret. From the registered endpoint, or printed by stripe listen during development. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your membership service's own database, separate from Discourse's. MEMBERSHIP_DATABASE_URL=postgres://user:pass@localhost:5432/membership # Required. Which Stripe price maps to which Discourse group. The whole access model in one line. TIER_GROUP_MAP=basic:members,pro:pro-members
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because communities need engagement mechanics and member operations, not just threads.
xpolished member UX
xmobile apps
xpayments
xcourse/events integration
xautomations
xmigrations
xcommunity admin
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Circle pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| professional | $89/workspace | — | Unlimited members; 3 admins; 10 moderators; 20 spaces; 200 GB storage; 100 live-stream attendees; 15 live-room participants; 10 live hours/month; 20 workflows; 2,500 AI actions/month; 5,000 Admin API calls/month; 2% transaction fee |
| business | $199/workspace | — | Unlimited members; 5 admins; 15 moderators; 30 spaces; 500 GB storage; 200 live-stream attendees; 20 live-room participants; 15 live hours/month; unlimited workflows; 2,500 AI actions/month; 30,000 Admin API calls/month; 1% transaction fee |
| plus | custom | — | Unlimited admins and moderators; flexible spaces/live limits; 1 TB storage; 10,000 Email Hub contacts; 0.5% transaction fee |
free tierno free tier; 14-day trial with no credit card and a 30-day money-back guarantee
billingmonthly prices published; annual billing is offered, but public annual-equivalent prices were not verified
hidden costsCircle adds 2%/1%/0.5% transaction fees on top of payment-processor fees. Overage/add-on examples: $10 per 1,000 AI actions; Admin API overages at $0.005/$0.002 per call; Headless MAU overages at $0.50/$0.30; extra admin $10/month; 10 extra moderators $20/month; 10 extra spaces $20/month; +100 live attendees $20/month; +10 live hours $50/month; Email Hub 10,000 contacts starts at +$99/month.
verified 2026-08-12 · source ↗
Vibecode Circle
Kinda. The core of Circle is buildable in a weekend with the prompt on this page, but there are real gaps: polished member UX, mobile apps. Read the honest list above before committing.
How much does Circle cost?
Circle costs about $89/month (Professional, checked 2026-08-12), which is $1068 per year.
What do I lose by replacing Circle?
Honestly: polished member UX; mobile apps; payments; course/events integration; automations; migrations; community admin. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Circle?
Yes: Moodle (A school wearing work boots: courses, forums, calendars, badges and paid enrolment, without Circle's polished creator clubhouse.) The prompt is for when you want it exactly your way.