Vibecode Bubble
track this build5 phases, 15 steps, beginner friendly0%A specific Bubble app is often vibecodable now; rebuilding Bubble's editor, workflow engine, plugin ecosystem, hosting, and scaling layer is not.
You are building a lean indie version of Bubble. 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 ===== # Bubble · indie build One real web application built on Django, with the pieces Bubble hands you already present: user accounts and password reset, a data layer with migrations, an admin interface for your tables, form driven workflows, background jobs, Stripe payments and a deploy with backups. The worked example is a small booking marketplace; substitute your own domain in Phase 2. 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 | | --- | --- | --- | | Framework | Django 5 with Python 3.12 | authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells | | Interface | Django templates with HTMX for partial updates | you get interactive forms and live updates without adopting a separate frontend build and its deployment | | Database | PostgreSQL | Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late | | Background work | Django management commands driven by cron, moving to a queue only when something actually needs it | most Bubble workflows are a form submission and an email, not a distributed job system | | Payments | Stripe Checkout with webhooks | hosted checkout keeps card data off your servers and out of your compliance scope | | Hosting | One virtual server behind Caddy, or a managed platform | one server with a documented backup beats an architecture diagram at this size | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs Django. Older versions will work and will also expire sooner. - Get it: Install from python.org or with your package manager, then create a virtual environment in the project directory. - Verify: python3 --version prints 3.12 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: The production database. Developing on SQLite and deploying on PostgreSQL hides differences until the worst moment. - Get it: brew install postgresql@16, or your package manager, or run it in Docker. Create a database and a user for the project. - Verify: psql --version prints 16 or higher and you can connect to your project database - [ ] **Stripe account with test mode keys** · free in test mode; live payments cost per transaction, currently around 2.9 percent plus a fixed fee in the United States - Why: Phase 4 needs a publishable key, a secret key and a webhook signing secret. Test mode does everything except move real money. - Get it: Sign up at stripe.com, then Developers > API keys for the two keys. The webhook signing secret comes from the endpoint you register, or from the stripe listen command during development. - Verify: stripe listen prints a webhook signing secret beginning whsec_ - [ ] **A transactional email provider** · most providers have a free tier of a few thousand messages a month; verify current limits when you sign up - Why: Password reset and confirmations both need real delivery. The console backend makes broken email look fine. - Get it: Create an account with a transactional provider, verify a sending domain by adding the DNS records they give you, and take an API key or SMTP credentials. Domain verification is the slow part, so start it early. - Verify: A password reset email arrives at an inbox you own - [ ] **A domain name and a small server** (optional) · roughly 10 to 15 USD a year for a domain and 5 to 10 USD a month for a small server - Why: Product phase only. Needed for certificates and for anything anyone else will use. - Get it: Any registrar for the domain, any small virtual server provider for the host. One or two gigabytes of memory is enough for this. ## Quick start ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. - visual editor - workflow builder - plugin marketplace - hosting/scaling - nondeveloper maintenance - templates If one of those is essential to you, that is the reason to keep paying for Bubble, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bubble 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 the one web application you were going to build in Bubble, written in code with auth, data, workflows and hosting to replace Bubble. 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 clone Bubble. Do not build a visual editor, a drag and drop canvas, a plugin system or a workflow engine. Bubble is worth what it is worth because it is a platform, and a platform is years of work for an audience of one. Build the single application you actually wanted, in code, using a framework that already gives you the parts Bubble was giving you: authentication, a database with migrations, an admin interface and a deploy story. ### Stack (fixed, do not substitute) - Framework: Django 5 with Python 3.12 · authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells - Interface: Django templates with HTMX for partial updates · you get interactive forms and live updates without adopting a separate frontend build and its deployment - Database: PostgreSQL · Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late - Background work: Django management commands driven by cron, moving to a queue only when something actually needs it · most Bubble workflows are a form submission and an email, not a distributed job system - Payments: Stripe Checkout with webhooks · hosted checkout keeps card data off your servers and out of your compliance scope - Hosting: One virtual server behind Caddy, or a managed platform · one server with a documented backup beats an architecture diagram at this size ### Data model (create this before Phase 1) - The worked example is a booking marketplace. Replace these tables with your own domain in Phase 2 and keep the shape. - User · Django's built-in model, extended with a profile rather than replaced - listings(id, owner_id, title, description, price_cents, active, created_at) - availability(id, listing_id, starts_at, ends_at, booked) - bookings(id, listing_id, guest_id, starts_at, ends_at, state, total_cents, stripe_session_id) - messages(id, booking_id, sender_id, body, sent_at) - Every state-changing action is a form POST to a view. That is what a Bubble workflow compiles down to in a code project. ### Phase 1 · Project, accounts and the admin Build: Start a Django project with PostgreSQL configured. Use the built-in authentication views for login, logout and the password reset flow. Configure email so reset actually sends. Create a superuser and confirm the admin is reachable and locked down. This phase is the largest single thing Bubble was giving you for free. Trap: Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. Done when: A new account can be created, logged out of, and logged back into. A password reset email arrives at a real inbox and the link sets a new password. The admin is reachable for a superuser and returns a login page rather than data for anonymous visitors. DEBUG is False in the settings used for anything but local development. Do not build yet: No domain models and no payments. Accounts and the admin only. ### Phase 2 · Your data model and migrations Build: Replace the example models with your own. Put real constraints in the database: foreign keys, not-null, unique together where it matters, and check constraints for things like a positive price. Register the models in the admin so you can create and inspect records before any user-facing screen exists. Trap: Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. Done when: Migrations apply to an empty database and produce the intended schema. Attempting to insert a booking whose end is before its start fails at the database level, not just in the form. The admin lists and creates every model. Running makemigrations again reports no changes, meaning the models and the migrations agree. Do not build yet: No user-facing screens yet. The admin is your interface for now. ### Phase 3 · Screens and workflows Build: Build the list, detail and create screens with Django forms and templates, and use HTMX for the interactions that would otherwise need a page reload. Every workflow is a POST to a view that validates, writes in a transaction, and redirects. Enforce permissions in the view, not in the template. Trap: Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. Done when: A logged-in visitor can complete the main journey from listing to confirmed record without the admin. Posting to another user's object returns a 403 or a 404 rather than succeeding. A double submit of the same form does not create two records, because the write is idempotent or the redirect happens after a successful commit. Do not build yet: No payments and no background jobs. ### Phase 4 · Payments Build: Stripe Checkout for the payment itself, and a webhook endpoint that is the only place an order is marked paid. Verify the webhook signature. Store the Stripe session id on the record and make the handler idempotent, because Stripe will deliver the same event more than once. Trap: Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. Done when: A test-mode payment moves the record to paid only after the webhook arrives. Replaying the same webhook event twice leaves one paid record and does not double-fulfil. A webhook with a bad signature is rejected with a 400. Closing the browser tab immediately after paying still results in a paid record. Do not build yet: No deploy yet; keep using the Stripe test keys. ### Phase 5 · Background work and email Build: Move email sending, reminders and anything slow out of the request. Start with management commands on a schedule, which is enough for most of what a Bubble workflow was doing. Record every send attempt so a failure is a row you can look at rather than a log line you have lost. Trap: Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. Done when: A booking confirmation email is sent by a scheduled command, not during the request, and the request returns in well under a second. A send failure leaves a row marked failed with the error, and a rerun retries only the failed ones. Running the command twice does not send the same email twice. Do not build yet: No deploy hardening yet. ### Phase 6 · Deploy, backups and a restore you have done Build: Deploy behind Caddy with automatic certificates. Serve static files properly. Take a nightly PostgreSQL dump to somewhere off the server, and then restore it onto a second machine to prove the dump is real. Trap: A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. Done when: The site serves over HTTPS with a valid certificate and DEBUG off. A nightly dump lands off the server. A restore onto a clean machine produces a working application with the same row counts, and the date of that restore is written in the README. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor. It is the product, it is why people pay, and reproducing it is a multi-year project with an audience of one. - The plugin marketplace, which is other people's work and cannot be written into existence. - Bubble's hosting and scaling, which is a real operational service you are now performing yourself. - Letting non-technical colleagues change the application without a developer. That capability is the honest reason many teams stay, and writing the app in code removes it. ### README must contain - Which framework version and which database, and how to run the project locally in under ten minutes - That the webhook is the only thing that marks a payment complete - Every environment variable, what it does, and which ones are secret - The backup schedule, the destination, and the date of the last successful restore - What this application deliberately does not do compared with the Bubble app it replaced ===== AGENTS.md ===== # Agent instructions · Bubble indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Django 5 with Python 3.12, Django templates with HTMX for partial updates, PostgreSQL, Django management commands driven by cron, moving to a queue only when something actually needs it, Stripe Checkout with webhooks, One virtual server behind Caddy, or a managed platform. 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 - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ===== BUILD_PLAN.md ===== # Build plan · Bubble One real web application built on Django, with the pieces Bubble hands you already present: user accounts and password reset, a data layer with migrations, an admin interface for your tables, form driven workflows, background jobs, Stripe payments and a deploy with backups. The worked example is a small booking marketplace; substitute your own domain in Phase 2. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Project, accounts and the admin Signup, login, logout and password reset all work, and you can edit data through the admin, before you write a single feature. ### Steps 1. Create the project and point it at PostgreSQL Start the project, add psycopg, and set the database from environment variables. Never commit a settings file with credentials in it. Files: `config/settings.py`, `.env.example` ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` 2. Wire the built-in auth views and a real email backend Include django.contrib.auth.urls, add templates for login and the reset flow, and configure SMTP against a transactional provider. Send yourself a reset to prove it. Files: `templates/registration/` ```sh python manage.py createsuperuser ``` 3. Lock the admin down before you forget Move it off /admin, require staff status, and confirm an anonymous request gets a login page. Turn DEBUG off outside local development and set ALLOWED_HOSTS. Files: `config/urls.py` ### Done when - [ ] Signup, logout and login all succeed - [ ] A password reset email reaches a real inbox and the link works - [ ] An anonymous request to the admin returns a login page, not data - [ ] The application refuses to start with DEBUG True when the environment says production ### Watch out - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. ## Phase 2 · Your data model and migrations Your real tables exist, with constraints the database enforces rather than the form. ### Steps 1. Write the models with database constraints Define the models and add Meta.constraints for the invariants. A CheckConstraint for a positive price and an ordered date range costs three lines and holds forever. Files: `app/models.py` ```sh python manage.py makemigrations python manage.py migrate ``` 2. Register everything in the admin list_display, search_fields and list_filter for each model. This is the closest analogue to Bubble's data tab and it is worth ten minutes. Files: `app/admin.py` 3. Seed realistic data A management command that creates a few users, listings and bookings so every later screen has something to render and every query has rows to be slow against. Files: `app/management/commands/seed.py` ```sh python manage.py seed ``` ### Done when - [ ] Migrations apply cleanly to an empty database - [ ] A booking with an end before its start is rejected by the database - [ ] makemigrations reports no pending changes after migrate - [ ] The seed command populates every model and is safe to run twice ### Watch out - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. ## Phase 3 · Screens and workflows A visitor can complete the main journey end to end without you touching the admin. ### Steps 1. Build the core screens with forms List, detail and create. Use ModelForm so validation lives with the model. Keep templates thin. Files: `app/views.py`, `templates/` 2. Add HTMX for the interactive parts Load HTMX from a vendored file rather than a content delivery network, and return HTML fragments from views. No build step, no bundler. Files: `static/htmx.min.js` 3. Authorise in the view and test it A test that logs in as user B and posts to user A's object, expecting a refusal. This is the test that catches the mistake everyone makes. Files: `app/tests/test_permissions.py` ```sh python manage.py test ``` ### Done when - [ ] The main journey completes without touching the admin - [ ] Posting to another user's object is refused - [ ] A double form submit does not create two records - [ ] Every form shows field errors rather than a server error page on bad input ### Watch out - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. ## Phase 4 · Payments Money moves, and your database agrees with Stripe about what happened. ### Steps 1. Create a Checkout session server side Never trust a price from the client. Look the amount up from your own database and pass it to Stripe. Files: `app/payments.py` ```sh pip install stripe ``` 2. Handle the webhook as the only source of truth Verify the signature with the endpoint signing secret, then update state. Exempt this view from CSRF, because it is a server to server call, and rely on the signature instead. Files: `app/webhooks.py` 3. Make the handler idempotent and test replay Store the processed event id and ignore duplicates. Use the Stripe command line tool to replay an event and assert nothing changes the second time. ```sh stripe listen --forward-to localhost:8000/webhooks/stripe/ stripe trigger checkout.session.completed ``` ### Done when - [ ] A test payment marks the record paid only after the webhook - [ ] Replaying the same event leaves exactly one paid record - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in a paid record ### Watch out - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. ## Phase 5 · Background work and email The things that should not block a request stop blocking it, and failures are visible. ### Steps 1. Add an outbox table Rows with state, attempts and last error. Views write rows; the command sends them. This is what makes retries and visibility possible. Files: `app/models.py` 2. Write the send command Select pending rows, send, mark sent or failed with the error text. Cap attempts and back off. Files: `app/management/commands/send_outbox.py` ```sh python manage.py send_outbox ``` 3. Schedule it and prove idempotence A cron entry every minute. A test that runs the command twice against one pending row and asserts one send. ### Done when - [ ] The request returns without waiting on email - [ ] A failed send is a row with an error, retried on the next run - [ ] Running the command twice sends once - [ ] Attempts are capped so a permanently bad address stops being retried ### Watch out - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ## Not in this build - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. ## After v1, if you want it - A JSON API for the parts a mobile client would need - Full text search using PostgreSQL rather than adding a search service - A staging environment restored from last night's production dump ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. DJANGO_SECRET_KEY=replace-with-50-random-characters # Required. Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. DJANGO_DEBUG=0 # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/appdb # Required. The hostnames this application answers to. Django refuses anything else, which is the point. ALLOWED_HOSTS=example.com,www.example.com # Optional · secret. Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Optional · secret. Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your transactional email provider's SMTP credentials. EMAIL_URL=smtp://user:pass@smtp.provider.com:587
You are building a lean indie version of Bubble. 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 ===== # Bubble · indie build One real web application built on Django, with the pieces Bubble hands you already present: user accounts and password reset, a data layer with migrations, an admin interface for your tables, form driven workflows, background jobs, Stripe payments and a deploy with backups. The worked example is a small booking marketplace; substitute your own domain in Phase 2. 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 | | --- | --- | --- | | Framework | Django 5 with Python 3.12 | authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells | | Interface | Django templates with HTMX for partial updates | you get interactive forms and live updates without adopting a separate frontend build and its deployment | | Database | PostgreSQL | Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late | | Background work | Django management commands driven by cron, moving to a queue only when something actually needs it | most Bubble workflows are a form submission and an email, not a distributed job system | | Payments | Stripe Checkout with webhooks | hosted checkout keeps card data off your servers and out of your compliance scope | | Hosting | One virtual server behind Caddy, or a managed platform | one server with a documented backup beats an architecture diagram at this size | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs Django. Older versions will work and will also expire sooner. - Get it: Install from python.org or with your package manager, then create a virtual environment in the project directory. - Verify: python3 --version prints 3.12 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: The production database. Developing on SQLite and deploying on PostgreSQL hides differences until the worst moment. - Get it: brew install postgresql@16, or your package manager, or run it in Docker. Create a database and a user for the project. - Verify: psql --version prints 16 or higher and you can connect to your project database - [ ] **Stripe account with test mode keys** · free in test mode; live payments cost per transaction, currently around 2.9 percent plus a fixed fee in the United States - Why: Phase 4 needs a publishable key, a secret key and a webhook signing secret. Test mode does everything except move real money. - Get it: Sign up at stripe.com, then Developers > API keys for the two keys. The webhook signing secret comes from the endpoint you register, or from the stripe listen command during development. - Verify: stripe listen prints a webhook signing secret beginning whsec_ - [ ] **A transactional email provider** · most providers have a free tier of a few thousand messages a month; verify current limits when you sign up - Why: Password reset and confirmations both need real delivery. The console backend makes broken email look fine. - Get it: Create an account with a transactional provider, verify a sending domain by adding the DNS records they give you, and take an API key or SMTP credentials. Domain verification is the slow part, so start it early. - Verify: A password reset email arrives at an inbox you own - [ ] **A domain name and a small server** (optional) · roughly 10 to 15 USD a year for a domain and 5 to 10 USD a month for a small server - Why: Product phase only. Needed for certificates and for anything anyone else will use. - Get it: Any registrar for the domain, any small virtual server provider for the host. One or two gigabytes of memory is enough for this. ## Quick start ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. - visual editor - workflow builder - plugin marketplace - hosting/scaling - nondeveloper maintenance - templates If one of those is essential to you, that is the reason to keep paying for Bubble, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bubble 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 the one web application you were going to build in Bubble, written in code with auth, data, workflows and hosting to replace Bubble. 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 clone Bubble. Do not build a visual editor, a drag and drop canvas, a plugin system or a workflow engine. Bubble is worth what it is worth because it is a platform, and a platform is years of work for an audience of one. Build the single application you actually wanted, in code, using a framework that already gives you the parts Bubble was giving you: authentication, a database with migrations, an admin interface and a deploy story. ### Stack (fixed, do not substitute) - Framework: Django 5 with Python 3.12 · authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells - Interface: Django templates with HTMX for partial updates · you get interactive forms and live updates without adopting a separate frontend build and its deployment - Database: PostgreSQL · Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late - Background work: Django management commands driven by cron, moving to a queue only when something actually needs it · most Bubble workflows are a form submission and an email, not a distributed job system - Payments: Stripe Checkout with webhooks · hosted checkout keeps card data off your servers and out of your compliance scope - Hosting: One virtual server behind Caddy, or a managed platform · one server with a documented backup beats an architecture diagram at this size ### Data model (create this before Phase 1) - The worked example is a booking marketplace. Replace these tables with your own domain in Phase 2 and keep the shape. - User · Django's built-in model, extended with a profile rather than replaced - listings(id, owner_id, title, description, price_cents, active, created_at) - availability(id, listing_id, starts_at, ends_at, booked) - bookings(id, listing_id, guest_id, starts_at, ends_at, state, total_cents, stripe_session_id) - messages(id, booking_id, sender_id, body, sent_at) - Every state-changing action is a form POST to a view. That is what a Bubble workflow compiles down to in a code project. ### Phase 1 · Project, accounts and the admin Build: Start a Django project with PostgreSQL configured. Use the built-in authentication views for login, logout and the password reset flow. Configure email so reset actually sends. Create a superuser and confirm the admin is reachable and locked down. This phase is the largest single thing Bubble was giving you for free. Trap: Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. Done when: A new account can be created, logged out of, and logged back into. A password reset email arrives at a real inbox and the link sets a new password. The admin is reachable for a superuser and returns a login page rather than data for anonymous visitors. DEBUG is False in the settings used for anything but local development. Do not build yet: No domain models and no payments. Accounts and the admin only. ### Phase 2 · Your data model and migrations Build: Replace the example models with your own. Put real constraints in the database: foreign keys, not-null, unique together where it matters, and check constraints for things like a positive price. Register the models in the admin so you can create and inspect records before any user-facing screen exists. Trap: Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. Done when: Migrations apply to an empty database and produce the intended schema. Attempting to insert a booking whose end is before its start fails at the database level, not just in the form. The admin lists and creates every model. Running makemigrations again reports no changes, meaning the models and the migrations agree. Do not build yet: No user-facing screens yet. The admin is your interface for now. ### Phase 3 · Screens and workflows Build: Build the list, detail and create screens with Django forms and templates, and use HTMX for the interactions that would otherwise need a page reload. Every workflow is a POST to a view that validates, writes in a transaction, and redirects. Enforce permissions in the view, not in the template. Trap: Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. Done when: A logged-in visitor can complete the main journey from listing to confirmed record without the admin. Posting to another user's object returns a 403 or a 404 rather than succeeding. A double submit of the same form does not create two records, because the write is idempotent or the redirect happens after a successful commit. Do not build yet: No payments and no background jobs. ### Phase 4 · Payments Build: Stripe Checkout for the payment itself, and a webhook endpoint that is the only place an order is marked paid. Verify the webhook signature. Store the Stripe session id on the record and make the handler idempotent, because Stripe will deliver the same event more than once. Trap: Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. Done when: A test-mode payment moves the record to paid only after the webhook arrives. Replaying the same webhook event twice leaves one paid record and does not double-fulfil. A webhook with a bad signature is rejected with a 400. Closing the browser tab immediately after paying still results in a paid record. Do not build yet: No deploy yet; keep using the Stripe test keys. ### Phase 5 · Background work and email Build: Move email sending, reminders and anything slow out of the request. Start with management commands on a schedule, which is enough for most of what a Bubble workflow was doing. Record every send attempt so a failure is a row you can look at rather than a log line you have lost. Trap: Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. Done when: A booking confirmation email is sent by a scheduled command, not during the request, and the request returns in well under a second. A send failure leaves a row marked failed with the error, and a rerun retries only the failed ones. Running the command twice does not send the same email twice. Do not build yet: No deploy hardening yet. ### Phase 6 · Deploy, backups and a restore you have done Build: Deploy behind Caddy with automatic certificates. Serve static files properly. Take a nightly PostgreSQL dump to somewhere off the server, and then restore it onto a second machine to prove the dump is real. Trap: A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. Done when: The site serves over HTTPS with a valid certificate and DEBUG off. A nightly dump lands off the server. A restore onto a clean machine produces a working application with the same row counts, and the date of that restore is written in the README. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor. It is the product, it is why people pay, and reproducing it is a multi-year project with an audience of one. - The plugin marketplace, which is other people's work and cannot be written into existence. - Bubble's hosting and scaling, which is a real operational service you are now performing yourself. - Letting non-technical colleagues change the application without a developer. That capability is the honest reason many teams stay, and writing the app in code removes it. ### README must contain - Which framework version and which database, and how to run the project locally in under ten minutes - That the webhook is the only thing that marks a payment complete - Every environment variable, what it does, and which ones are secret - The backup schedule, the destination, and the date of the last successful restore - What this application deliberately does not do compared with the Bubble app it replaced ===== AGENTS.md ===== # Agent instructions · Bubble indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Django 5 with Python 3.12, Django templates with HTMX for partial updates, PostgreSQL, Django management commands driven by cron, moving to a queue only when something actually needs it, Stripe Checkout with webhooks, One virtual server behind Caddy, or a managed platform. 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 - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ===== BUILD_PLAN.md ===== # Build plan · Bubble One real web application built on Django, with the pieces Bubble hands you already present: user accounts and password reset, a data layer with migrations, an admin interface for your tables, form driven workflows, background jobs, Stripe payments and a deploy with backups. The worked example is a small booking marketplace; substitute your own domain in Phase 2. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Project, accounts and the admin Signup, login, logout and password reset all work, and you can edit data through the admin, before you write a single feature. ### Steps 1. Create the project and point it at PostgreSQL Start the project, add psycopg, and set the database from environment variables. Never commit a settings file with credentials in it. Files: `config/settings.py`, `.env.example` ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` 2. Wire the built-in auth views and a real email backend Include django.contrib.auth.urls, add templates for login and the reset flow, and configure SMTP against a transactional provider. Send yourself a reset to prove it. Files: `templates/registration/` ```sh python manage.py createsuperuser ``` 3. Lock the admin down before you forget Move it off /admin, require staff status, and confirm an anonymous request gets a login page. Turn DEBUG off outside local development and set ALLOWED_HOSTS. Files: `config/urls.py` ### Done when - [ ] Signup, logout and login all succeed - [ ] A password reset email reaches a real inbox and the link works - [ ] An anonymous request to the admin returns a login page, not data - [ ] The application refuses to start with DEBUG True when the environment says production ### Watch out - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. ## Phase 2 · Your data model and migrations Your real tables exist, with constraints the database enforces rather than the form. ### Steps 1. Write the models with database constraints Define the models and add Meta.constraints for the invariants. A CheckConstraint for a positive price and an ordered date range costs three lines and holds forever. Files: `app/models.py` ```sh python manage.py makemigrations python manage.py migrate ``` 2. Register everything in the admin list_display, search_fields and list_filter for each model. This is the closest analogue to Bubble's data tab and it is worth ten minutes. Files: `app/admin.py` 3. Seed realistic data A management command that creates a few users, listings and bookings so every later screen has something to render and every query has rows to be slow against. Files: `app/management/commands/seed.py` ```sh python manage.py seed ``` ### Done when - [ ] Migrations apply cleanly to an empty database - [ ] A booking with an end before its start is rejected by the database - [ ] makemigrations reports no pending changes after migrate - [ ] The seed command populates every model and is safe to run twice ### Watch out - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. ## Phase 3 · Screens and workflows A visitor can complete the main journey end to end without you touching the admin. ### Steps 1. Build the core screens with forms List, detail and create. Use ModelForm so validation lives with the model. Keep templates thin. Files: `app/views.py`, `templates/` 2. Add HTMX for the interactive parts Load HTMX from a vendored file rather than a content delivery network, and return HTML fragments from views. No build step, no bundler. Files: `static/htmx.min.js` 3. Authorise in the view and test it A test that logs in as user B and posts to user A's object, expecting a refusal. This is the test that catches the mistake everyone makes. Files: `app/tests/test_permissions.py` ```sh python manage.py test ``` ### Done when - [ ] The main journey completes without touching the admin - [ ] Posting to another user's object is refused - [ ] A double form submit does not create two records - [ ] Every form shows field errors rather than a server error page on bad input ### Watch out - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. ## Phase 4 · Payments Money moves, and your database agrees with Stripe about what happened. ### Steps 1. Create a Checkout session server side Never trust a price from the client. Look the amount up from your own database and pass it to Stripe. Files: `app/payments.py` ```sh pip install stripe ``` 2. Handle the webhook as the only source of truth Verify the signature with the endpoint signing secret, then update state. Exempt this view from CSRF, because it is a server to server call, and rely on the signature instead. Files: `app/webhooks.py` 3. Make the handler idempotent and test replay Store the processed event id and ignore duplicates. Use the Stripe command line tool to replay an event and assert nothing changes the second time. ```sh stripe listen --forward-to localhost:8000/webhooks/stripe/ stripe trigger checkout.session.completed ``` ### Done when - [ ] A test payment marks the record paid only after the webhook - [ ] Replaying the same event leaves exactly one paid record - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in a paid record ### Watch out - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. ## Phase 5 · Background work and email The things that should not block a request stop blocking it, and failures are visible. ### Steps 1. Add an outbox table Rows with state, attempts and last error. Views write rows; the command sends them. This is what makes retries and visibility possible. Files: `app/models.py` 2. Write the send command Select pending rows, send, mark sent or failed with the error text. Cap attempts and back off. Files: `app/management/commands/send_outbox.py` ```sh python manage.py send_outbox ``` 3. Schedule it and prove idempotence A cron entry every minute. A test that runs the command twice against one pending row and asserts one send. ### Done when - [ ] The request returns without waiting on email - [ ] A failed send is a row with an error, retried on the next run - [ ] Running the command twice sends once - [ ] Attempts are capped so a permanently bad address stops being retried ### Watch out - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ## Not in this build - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. ## After v1, if you want it - A JSON API for the parts a mobile client would need - Full text search using PostgreSQL rather than adding a search service - A staging environment restored from last night's production dump ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. DJANGO_SECRET_KEY=replace-with-50-random-characters # Required. Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. DJANGO_DEBUG=0 # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/appdb # Required. The hostnames this application answers to. Django refuses anything else, which is the point. ALLOWED_HOSTS=example.com,www.example.com # Optional · secret. Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Optional · secret. Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your transactional email provider's SMTP credentials. EMAIL_URL=smtp://user:pass@smtp.provider.com:587
You are building a production product version of Bubble. 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 ===== # Bubble · product brief ## Problem A specific Bubble app is often vibecodable now; rebuilding Bubble's editor, workflow engine, plugin ecosystem, hosting, and scaling layer is not. ## Product outcome One real application you own outright, running on your own infrastructure, with payments that reconcile, background work that is visible when it fails, and a restore you have performed rather than assumed. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - web framework - database - auth - hosting - payment/email APIs - product-specific integrations ## Explicit non-goals for v1 - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. - visual editor - workflow builder - plugin marketplace - hosting/scaling - nondeveloper maintenance - templates ## Success criteria - DEBUG is off, ALLOWED_HOSTS is set, and the secret key came from the environment - The permission test that posts to another user's object still passes - A replayed Stripe webhook does not double-fulfil - A restore onto a clean machine has been performed and dated - Every environment variable is documented, and no secret is committed ===== BRIEF.md ===== # Build brief · Bubble 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 the one web application you were going to build in Bubble, written in code with auth, data, workflows and hosting to replace Bubble. 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 clone Bubble. Do not build a visual editor, a drag and drop canvas, a plugin system or a workflow engine. Bubble is worth what it is worth because it is a platform, and a platform is years of work for an audience of one. Build the single application you actually wanted, in code, using a framework that already gives you the parts Bubble was giving you: authentication, a database with migrations, an admin interface and a deploy story. ### Stack (fixed, do not substitute) - Framework: Django 5 with Python 3.12 · authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells - Interface: Django templates with HTMX for partial updates · you get interactive forms and live updates without adopting a separate frontend build and its deployment - Database: PostgreSQL · Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late - Background work: Django management commands driven by cron, moving to a queue only when something actually needs it · most Bubble workflows are a form submission and an email, not a distributed job system - Payments: Stripe Checkout with webhooks · hosted checkout keeps card data off your servers and out of your compliance scope - Hosting: One virtual server behind Caddy, or a managed platform · one server with a documented backup beats an architecture diagram at this size ### Data model (create this before Phase 1) - The worked example is a booking marketplace. Replace these tables with your own domain in Phase 2 and keep the shape. - User · Django's built-in model, extended with a profile rather than replaced - listings(id, owner_id, title, description, price_cents, active, created_at) - availability(id, listing_id, starts_at, ends_at, booked) - bookings(id, listing_id, guest_id, starts_at, ends_at, state, total_cents, stripe_session_id) - messages(id, booking_id, sender_id, body, sent_at) - Every state-changing action is a form POST to a view. That is what a Bubble workflow compiles down to in a code project. ### Phase 1 · Project, accounts and the admin Build: Start a Django project with PostgreSQL configured. Use the built-in authentication views for login, logout and the password reset flow. Configure email so reset actually sends. Create a superuser and confirm the admin is reachable and locked down. This phase is the largest single thing Bubble was giving you for free. Trap: Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. Done when: A new account can be created, logged out of, and logged back into. A password reset email arrives at a real inbox and the link sets a new password. The admin is reachable for a superuser and returns a login page rather than data for anonymous visitors. DEBUG is False in the settings used for anything but local development. Do not build yet: No domain models and no payments. Accounts and the admin only. ### Phase 2 · Your data model and migrations Build: Replace the example models with your own. Put real constraints in the database: foreign keys, not-null, unique together where it matters, and check constraints for things like a positive price. Register the models in the admin so you can create and inspect records before any user-facing screen exists. Trap: Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. Done when: Migrations apply to an empty database and produce the intended schema. Attempting to insert a booking whose end is before its start fails at the database level, not just in the form. The admin lists and creates every model. Running makemigrations again reports no changes, meaning the models and the migrations agree. Do not build yet: No user-facing screens yet. The admin is your interface for now. ### Phase 3 · Screens and workflows Build: Build the list, detail and create screens with Django forms and templates, and use HTMX for the interactions that would otherwise need a page reload. Every workflow is a POST to a view that validates, writes in a transaction, and redirects. Enforce permissions in the view, not in the template. Trap: Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. Done when: A logged-in visitor can complete the main journey from listing to confirmed record without the admin. Posting to another user's object returns a 403 or a 404 rather than succeeding. A double submit of the same form does not create two records, because the write is idempotent or the redirect happens after a successful commit. Do not build yet: No payments and no background jobs. ### Phase 4 · Payments Build: Stripe Checkout for the payment itself, and a webhook endpoint that is the only place an order is marked paid. Verify the webhook signature. Store the Stripe session id on the record and make the handler idempotent, because Stripe will deliver the same event more than once. Trap: Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. Done when: A test-mode payment moves the record to paid only after the webhook arrives. Replaying the same webhook event twice leaves one paid record and does not double-fulfil. A webhook with a bad signature is rejected with a 400. Closing the browser tab immediately after paying still results in a paid record. Do not build yet: No deploy yet; keep using the Stripe test keys. ### Phase 5 · Background work and email Build: Move email sending, reminders and anything slow out of the request. Start with management commands on a schedule, which is enough for most of what a Bubble workflow was doing. Record every send attempt so a failure is a row you can look at rather than a log line you have lost. Trap: Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. Done when: A booking confirmation email is sent by a scheduled command, not during the request, and the request returns in well under a second. A send failure leaves a row marked failed with the error, and a rerun retries only the failed ones. Running the command twice does not send the same email twice. Do not build yet: No deploy hardening yet. ### Phase 6 · Deploy, backups and a restore you have done Build: Deploy behind Caddy with automatic certificates. Serve static files properly. Take a nightly PostgreSQL dump to somewhere off the server, and then restore it onto a second machine to prove the dump is real. Trap: A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. Done when: The site serves over HTTPS with a valid certificate and DEBUG off. A nightly dump lands off the server. A restore onto a clean machine produces a working application with the same row counts, and the date of that restore is written in the README. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor. It is the product, it is why people pay, and reproducing it is a multi-year project with an audience of one. - The plugin marketplace, which is other people's work and cannot be written into existence. - Bubble's hosting and scaling, which is a real operational service you are now performing yourself. - Letting non-technical colleagues change the application without a developer. That capability is the honest reason many teams stay, and writing the app in code removes it. ### README must contain - Which framework version and which database, and how to run the project locally in under ten minutes - That the webhook is the only thing that marks a payment complete - Every environment variable, what it does, and which ones are secret - The backup schedule, the destination, and the date of the last successful restore - What this application deliberately does not do compared with the Bubble app it replaced ===== ARCHITECTURE.md ===== # Architecture · Bubble ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Django 5 with Python 3.12 | authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells | | Interface | Django templates with HTMX for partial updates | you get interactive forms and live updates without adopting a separate frontend build and its deployment | | Database | PostgreSQL | Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late | | Background work | Django management commands driven by cron, moving to a queue only when something actually needs it | most Bubble workflows are a form submission and an email, not a distributed job system | | Payments | Stripe Checkout with webhooks | hosted checkout keeps card data off your servers and out of your compliance scope | | Hosting | One virtual server behind Caddy, or a managed platform | one server with a documented backup beats an architecture diagram at this size | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Auth | Accounts, sessions, password reset. | Django's own. Replace with an external identity provider only if you need single sign-on, and expect that to touch every view that reads the user. | | Domain models | Your tables and the constraints that keep them honest. | This is the part that is genuinely yours. Everything else is replaceable; this is not. | | Views and templates | Screens, forms and authorisation. | A JSON API and a separate frontend is the usual next step. Keep authorisation in the view layer so that swap does not lose it. | | Payments | Checkout sessions and the webhook that is the source of truth. | Another processor is a different session call and a different signature check. The idempotent webhook handler shape survives the change. | | Outbox | Deferred sending with retries and visible failures. | Move to a real queue when latency or retry timing demands it. The outbox table stays useful either way. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DJANGO_SECRET_KEY` · required, secret · Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. - `DJANGO_DEBUG` · required · Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `ALLOWED_HOSTS` · required · The hostnames this application answers to. Django refuses anything else, which is the point. - `STRIPE_SECRET_KEY` · optional, secret · Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. - `STRIPE_WEBHOOK_SECRET` · optional, secret · Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. - `EMAIL_URL` · required, secret · Your transactional email provider's SMTP credentials. ## 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 · Bubble product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Django 5 with Python 3.12, Django templates with HTMX for partial updates, PostgreSQL, Django management commands driven by cron, moving to a queue only when something actually needs it, Stripe Checkout with webhooks, One virtual server behind Caddy, or a managed platform. - 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 - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. - A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. ===== MILESTONES.md ===== # Delivery milestones · Bubble Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Project, accounts and the admin Signup, login, logout and password reset all work, and you can edit data through the admin, before you write a single feature. ### Steps 1. Create the project and point it at PostgreSQL Start the project, add psycopg, and set the database from environment variables. Never commit a settings file with credentials in it. Files: `config/settings.py`, `.env.example` ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` 2. Wire the built-in auth views and a real email backend Include django.contrib.auth.urls, add templates for login and the reset flow, and configure SMTP against a transactional provider. Send yourself a reset to prove it. Files: `templates/registration/` ```sh python manage.py createsuperuser ``` 3. Lock the admin down before you forget Move it off /admin, require staff status, and confirm an anonymous request gets a login page. Turn DEBUG off outside local development and set ALLOWED_HOSTS. Files: `config/urls.py` ### Done when - [ ] Signup, logout and login all succeed - [ ] A password reset email reaches a real inbox and the link works - [ ] An anonymous request to the admin returns a login page, not data - [ ] The application refuses to start with DEBUG True when the environment says production ### Watch out - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. ## M2 · Your data model and migrations Your real tables exist, with constraints the database enforces rather than the form. ### Steps 1. Write the models with database constraints Define the models and add Meta.constraints for the invariants. A CheckConstraint for a positive price and an ordered date range costs three lines and holds forever. Files: `app/models.py` ```sh python manage.py makemigrations python manage.py migrate ``` 2. Register everything in the admin list_display, search_fields and list_filter for each model. This is the closest analogue to Bubble's data tab and it is worth ten minutes. Files: `app/admin.py` 3. Seed realistic data A management command that creates a few users, listings and bookings so every later screen has something to render and every query has rows to be slow against. Files: `app/management/commands/seed.py` ```sh python manage.py seed ``` ### Done when - [ ] Migrations apply cleanly to an empty database - [ ] A booking with an end before its start is rejected by the database - [ ] makemigrations reports no pending changes after migrate - [ ] The seed command populates every model and is safe to run twice ### Watch out - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. ## M3 · Screens and workflows A visitor can complete the main journey end to end without you touching the admin. ### Steps 1. Build the core screens with forms List, detail and create. Use ModelForm so validation lives with the model. Keep templates thin. Files: `app/views.py`, `templates/` 2. Add HTMX for the interactive parts Load HTMX from a vendored file rather than a content delivery network, and return HTML fragments from views. No build step, no bundler. Files: `static/htmx.min.js` 3. Authorise in the view and test it A test that logs in as user B and posts to user A's object, expecting a refusal. This is the test that catches the mistake everyone makes. Files: `app/tests/test_permissions.py` ```sh python manage.py test ``` ### Done when - [ ] The main journey completes without touching the admin - [ ] Posting to another user's object is refused - [ ] A double form submit does not create two records - [ ] Every form shows field errors rather than a server error page on bad input ### Watch out - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. ## M4 · Payments Money moves, and your database agrees with Stripe about what happened. ### Steps 1. Create a Checkout session server side Never trust a price from the client. Look the amount up from your own database and pass it to Stripe. Files: `app/payments.py` ```sh pip install stripe ``` 2. Handle the webhook as the only source of truth Verify the signature with the endpoint signing secret, then update state. Exempt this view from CSRF, because it is a server to server call, and rely on the signature instead. Files: `app/webhooks.py` 3. Make the handler idempotent and test replay Store the processed event id and ignore duplicates. Use the Stripe command line tool to replay an event and assert nothing changes the second time. ```sh stripe listen --forward-to localhost:8000/webhooks/stripe/ stripe trigger checkout.session.completed ``` ### Done when - [ ] A test payment marks the record paid only after the webhook - [ ] Replaying the same event leaves exactly one paid record - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in a paid record ### Watch out - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. ## M5 · Background work and email The things that should not block a request stop blocking it, and failures are visible. ### Steps 1. Add an outbox table Rows with state, attempts and last error. Views write rows; the command sends them. This is what makes retries and visibility possible. Files: `app/models.py` 2. Write the send command Select pending rows, send, mark sent or failed with the error text. Cap attempts and back off. Files: `app/management/commands/send_outbox.py` ```sh python manage.py send_outbox ``` 3. Schedule it and prove idempotence A cron entry every minute. A test that runs the command twice against one pending row and asserts one send. ### Done when - [ ] The request returns without waiting on email - [ ] A failed send is a row with an error, retried on the next run - [ ] Running the command twice sends once - [ ] Attempts are capped so a permanently bad address stops being retried ### Watch out - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ## M6 · Deploy, backups and a restore you have done (production only) It runs somewhere other than your laptop, and you have proved you can bring it back. ### Steps 1. Deploy behind Caddy with a real certificate Gunicorn behind Caddy, static files collected and served by Caddy, secrets from the environment, DEBUG off and ALLOWED_HOSTS set. Files: `Caddyfile` ```sh python manage.py collectstatic ``` 2. Automate an off-server dump pg_dump on a schedule, encrypted or to a destination only you control, keeping several generations. ```sh pg_dump "$DATABASE_URL" | gzip > backup-$(date +%F).sql.gz ``` 3. Perform the restore drill and date it Restore the newest dump onto a second machine, run migrations, and compare row counts. Write the date in the README. Files: `README.md` ```sh gunzip -c backup.sql.gz | psql "$RESTORE_URL" ``` ### Done when - [ ] The site serves over HTTPS with a valid certificate - [ ] DEBUG is off and the admin is not at the default path - [ ] A dump exists off the server and can be listed from another machine - [ ] A restore was performed on a clean machine and the date is recorded ### Watch out - A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. ===== OPERATIONS.md ===== # Operations · Bubble ## Backup Nightly pg_dump to a destination off the server, several generations retained. Uploaded files, if any, need their own copy; a database dump does not contain them. ## Restore Create a database, load the newest dump, run migrations, point the application at it, and compare row counts against what the dump claimed. Do this on a schedule, not only in an emergency. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring An uptime check against a health endpoint that touches the database, plus an alert if the outbox has rows older than an hour, which is how you learn email broke without reading logs. ## Incident checklist Keep the previous release deployable. For a bad migration, restore the dump taken immediately before it rather than writing a reverse migration under pressure. For a payment dispute, the webhook event log is your record of what actually happened. 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 - [ ] DEBUG is off, ALLOWED_HOSTS is set, and the secret key came from the environment - [ ] The permission test that posts to another user's object still passes - [ ] A replayed Stripe webhook does not double-fulfil - [ ] A restore onto a clean machine has been performed and dated - [ ] Every environment variable is documented, and no secret is committed ## Launch constraint Do not market omitted Bubble 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 · secret. Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. DJANGO_SECRET_KEY=replace-with-50-random-characters # Required. Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. DJANGO_DEBUG=0 # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/appdb # Required. The hostnames this application answers to. Django refuses anything else, which is the point. ALLOWED_HOSTS=example.com,www.example.com # Optional · secret. Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Optional · secret. Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your transactional email provider's SMTP credentials. EMAIL_URL=smtp://user:pass@smtp.provider.com:587
# Bubble · indie build One real web application built on Django, with the pieces Bubble hands you already present: user accounts and password reset, a data layer with migrations, an admin interface for your tables, form driven workflows, background jobs, Stripe payments and a deploy with backups. The worked example is a small booking marketplace; substitute your own domain in Phase 2. 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 | | --- | --- | --- | | Framework | Django 5 with Python 3.12 | authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells | | Interface | Django templates with HTMX for partial updates | you get interactive forms and live updates without adopting a separate frontend build and its deployment | | Database | PostgreSQL | Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late | | Background work | Django management commands driven by cron, moving to a queue only when something actually needs it | most Bubble workflows are a form submission and an email, not a distributed job system | | Payments | Stripe Checkout with webhooks | hosted checkout keeps card data off your servers and out of your compliance scope | | Hosting | One virtual server behind Caddy, or a managed platform | one server with a documented backup beats an architecture diagram at this size | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs Django. Older versions will work and will also expire sooner. - Get it: Install from python.org or with your package manager, then create a virtual environment in the project directory. - Verify: python3 --version prints 3.12 or higher - [ ] **PostgreSQL 16 or newer** · free - Why: The production database. Developing on SQLite and deploying on PostgreSQL hides differences until the worst moment. - Get it: brew install postgresql@16, or your package manager, or run it in Docker. Create a database and a user for the project. - Verify: psql --version prints 16 or higher and you can connect to your project database - [ ] **Stripe account with test mode keys** · free in test mode; live payments cost per transaction, currently around 2.9 percent plus a fixed fee in the United States - Why: Phase 4 needs a publishable key, a secret key and a webhook signing secret. Test mode does everything except move real money. - Get it: Sign up at stripe.com, then Developers > API keys for the two keys. The webhook signing secret comes from the endpoint you register, or from the stripe listen command during development. - Verify: stripe listen prints a webhook signing secret beginning whsec_ - [ ] **A transactional email provider** · most providers have a free tier of a few thousand messages a month; verify current limits when you sign up - Why: Password reset and confirmations both need real delivery. The console backend makes broken email look fine. - Get it: Create an account with a transactional provider, verify a sending domain by adding the DNS records they give you, and take an API key or SMTP credentials. Domain verification is the slow part, so start it early. - Verify: A password reset email arrives at an inbox you own - [ ] **A domain name and a small server** (optional) · roughly 10 to 15 USD a year for a domain and 5 to 10 USD a month for a small server - Why: Product phase only. Needed for certificates and for anything anyone else will use. - Get it: Any registrar for the domain, any small virtual server provider for the host. One or two gigabytes of memory is enough for this. ## Quick start ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. - visual editor - workflow builder - plugin marketplace - hosting/scaling - nondeveloper maintenance - templates If one of those is essential to you, that is the reason to keep paying for Bubble, and the README should say so rather than pretend.
# Build brief · Bubble 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 the one web application you were going to build in Bubble, written in code with auth, data, workflows and hosting to replace Bubble. 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 clone Bubble. Do not build a visual editor, a drag and drop canvas, a plugin system or a workflow engine. Bubble is worth what it is worth because it is a platform, and a platform is years of work for an audience of one. Build the single application you actually wanted, in code, using a framework that already gives you the parts Bubble was giving you: authentication, a database with migrations, an admin interface and a deploy story. ### Stack (fixed, do not substitute) - Framework: Django 5 with Python 3.12 · authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells - Interface: Django templates with HTMX for partial updates · you get interactive forms and live updates without adopting a separate frontend build and its deployment - Database: PostgreSQL · Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late - Background work: Django management commands driven by cron, moving to a queue only when something actually needs it · most Bubble workflows are a form submission and an email, not a distributed job system - Payments: Stripe Checkout with webhooks · hosted checkout keeps card data off your servers and out of your compliance scope - Hosting: One virtual server behind Caddy, or a managed platform · one server with a documented backup beats an architecture diagram at this size ### Data model (create this before Phase 1) - The worked example is a booking marketplace. Replace these tables with your own domain in Phase 2 and keep the shape. - User · Django's built-in model, extended with a profile rather than replaced - listings(id, owner_id, title, description, price_cents, active, created_at) - availability(id, listing_id, starts_at, ends_at, booked) - bookings(id, listing_id, guest_id, starts_at, ends_at, state, total_cents, stripe_session_id) - messages(id, booking_id, sender_id, body, sent_at) - Every state-changing action is a form POST to a view. That is what a Bubble workflow compiles down to in a code project. ### Phase 1 · Project, accounts and the admin Build: Start a Django project with PostgreSQL configured. Use the built-in authentication views for login, logout and the password reset flow. Configure email so reset actually sends. Create a superuser and confirm the admin is reachable and locked down. This phase is the largest single thing Bubble was giving you for free. Trap: Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. Done when: A new account can be created, logged out of, and logged back into. A password reset email arrives at a real inbox and the link sets a new password. The admin is reachable for a superuser and returns a login page rather than data for anonymous visitors. DEBUG is False in the settings used for anything but local development. Do not build yet: No domain models and no payments. Accounts and the admin only. ### Phase 2 · Your data model and migrations Build: Replace the example models with your own. Put real constraints in the database: foreign keys, not-null, unique together where it matters, and check constraints for things like a positive price. Register the models in the admin so you can create and inspect records before any user-facing screen exists. Trap: Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. Done when: Migrations apply to an empty database and produce the intended schema. Attempting to insert a booking whose end is before its start fails at the database level, not just in the form. The admin lists and creates every model. Running makemigrations again reports no changes, meaning the models and the migrations agree. Do not build yet: No user-facing screens yet. The admin is your interface for now. ### Phase 3 · Screens and workflows Build: Build the list, detail and create screens with Django forms and templates, and use HTMX for the interactions that would otherwise need a page reload. Every workflow is a POST to a view that validates, writes in a transaction, and redirects. Enforce permissions in the view, not in the template. Trap: Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. Done when: A logged-in visitor can complete the main journey from listing to confirmed record without the admin. Posting to another user's object returns a 403 or a 404 rather than succeeding. A double submit of the same form does not create two records, because the write is idempotent or the redirect happens after a successful commit. Do not build yet: No payments and no background jobs. ### Phase 4 · Payments Build: Stripe Checkout for the payment itself, and a webhook endpoint that is the only place an order is marked paid. Verify the webhook signature. Store the Stripe session id on the record and make the handler idempotent, because Stripe will deliver the same event more than once. Trap: Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. Done when: A test-mode payment moves the record to paid only after the webhook arrives. Replaying the same webhook event twice leaves one paid record and does not double-fulfil. A webhook with a bad signature is rejected with a 400. Closing the browser tab immediately after paying still results in a paid record. Do not build yet: No deploy yet; keep using the Stripe test keys. ### Phase 5 · Background work and email Build: Move email sending, reminders and anything slow out of the request. Start with management commands on a schedule, which is enough for most of what a Bubble workflow was doing. Record every send attempt so a failure is a row you can look at rather than a log line you have lost. Trap: Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. Done when: A booking confirmation email is sent by a scheduled command, not during the request, and the request returns in well under a second. A send failure leaves a row marked failed with the error, and a rerun retries only the failed ones. Running the command twice does not send the same email twice. Do not build yet: No deploy hardening yet. ### Phase 6 · Deploy, backups and a restore you have done Build: Deploy behind Caddy with automatic certificates. Serve static files properly. Take a nightly PostgreSQL dump to somewhere off the server, and then restore it onto a second machine to prove the dump is real. Trap: A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. Done when: The site serves over HTTPS with a valid certificate and DEBUG off. A nightly dump lands off the server. A restore onto a clean machine produces a working application with the same row counts, and the date of that restore is written in the README. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor. It is the product, it is why people pay, and reproducing it is a multi-year project with an audience of one. - The plugin marketplace, which is other people's work and cannot be written into existence. - Bubble's hosting and scaling, which is a real operational service you are now performing yourself. - Letting non-technical colleagues change the application without a developer. That capability is the honest reason many teams stay, and writing the app in code removes it. ### README must contain - Which framework version and which database, and how to run the project locally in under ten minutes - That the webhook is the only thing that marks a payment complete - Every environment variable, what it does, and which ones are secret - The backup schedule, the destination, and the date of the last successful restore - What this application deliberately does not do compared with the Bubble app it replaced
# Agent instructions · Bubble indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Django 5 with Python 3.12, Django templates with HTMX for partial updates, PostgreSQL, Django management commands driven by cron, moving to a queue only when something actually needs it, Stripe Checkout with webhooks, One virtual server behind Caddy, or a managed platform. 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 - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before.
# Build plan · Bubble One real web application built on Django, with the pieces Bubble hands you already present: user accounts and password reset, a data layer with migrations, an admin interface for your tables, form driven workflows, background jobs, Stripe payments and a deploy with backups. The worked example is a small booking marketplace; substitute your own domain in Phase 2. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Project, accounts and the admin Signup, login, logout and password reset all work, and you can edit data through the admin, before you write a single feature. ### Steps 1. Create the project and point it at PostgreSQL Start the project, add psycopg, and set the database from environment variables. Never commit a settings file with credentials in it. Files: `config/settings.py`, `.env.example` ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` 2. Wire the built-in auth views and a real email backend Include django.contrib.auth.urls, add templates for login and the reset flow, and configure SMTP against a transactional provider. Send yourself a reset to prove it. Files: `templates/registration/` ```sh python manage.py createsuperuser ``` 3. Lock the admin down before you forget Move it off /admin, require staff status, and confirm an anonymous request gets a login page. Turn DEBUG off outside local development and set ALLOWED_HOSTS. Files: `config/urls.py` ### Done when - [ ] Signup, logout and login all succeed - [ ] A password reset email reaches a real inbox and the link works - [ ] An anonymous request to the admin returns a login page, not data - [ ] The application refuses to start with DEBUG True when the environment says production ### Watch out - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. ## Phase 2 · Your data model and migrations Your real tables exist, with constraints the database enforces rather than the form. ### Steps 1. Write the models with database constraints Define the models and add Meta.constraints for the invariants. A CheckConstraint for a positive price and an ordered date range costs three lines and holds forever. Files: `app/models.py` ```sh python manage.py makemigrations python manage.py migrate ``` 2. Register everything in the admin list_display, search_fields and list_filter for each model. This is the closest analogue to Bubble's data tab and it is worth ten minutes. Files: `app/admin.py` 3. Seed realistic data A management command that creates a few users, listings and bookings so every later screen has something to render and every query has rows to be slow against. Files: `app/management/commands/seed.py` ```sh python manage.py seed ``` ### Done when - [ ] Migrations apply cleanly to an empty database - [ ] A booking with an end before its start is rejected by the database - [ ] makemigrations reports no pending changes after migrate - [ ] The seed command populates every model and is safe to run twice ### Watch out - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. ## Phase 3 · Screens and workflows A visitor can complete the main journey end to end without you touching the admin. ### Steps 1. Build the core screens with forms List, detail and create. Use ModelForm so validation lives with the model. Keep templates thin. Files: `app/views.py`, `templates/` 2. Add HTMX for the interactive parts Load HTMX from a vendored file rather than a content delivery network, and return HTML fragments from views. No build step, no bundler. Files: `static/htmx.min.js` 3. Authorise in the view and test it A test that logs in as user B and posts to user A's object, expecting a refusal. This is the test that catches the mistake everyone makes. Files: `app/tests/test_permissions.py` ```sh python manage.py test ``` ### Done when - [ ] The main journey completes without touching the admin - [ ] Posting to another user's object is refused - [ ] A double form submit does not create two records - [ ] Every form shows field errors rather than a server error page on bad input ### Watch out - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. ## Phase 4 · Payments Money moves, and your database agrees with Stripe about what happened. ### Steps 1. Create a Checkout session server side Never trust a price from the client. Look the amount up from your own database and pass it to Stripe. Files: `app/payments.py` ```sh pip install stripe ``` 2. Handle the webhook as the only source of truth Verify the signature with the endpoint signing secret, then update state. Exempt this view from CSRF, because it is a server to server call, and rely on the signature instead. Files: `app/webhooks.py` 3. Make the handler idempotent and test replay Store the processed event id and ignore duplicates. Use the Stripe command line tool to replay an event and assert nothing changes the second time. ```sh stripe listen --forward-to localhost:8000/webhooks/stripe/ stripe trigger checkout.session.completed ``` ### Done when - [ ] A test payment marks the record paid only after the webhook - [ ] Replaying the same event leaves exactly one paid record - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in a paid record ### Watch out - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. ## Phase 5 · Background work and email The things that should not block a request stop blocking it, and failures are visible. ### Steps 1. Add an outbox table Rows with state, attempts and last error. Views write rows; the command sends them. This is what makes retries and visibility possible. Files: `app/models.py` 2. Write the send command Select pending rows, send, mark sent or failed with the error text. Cap attempts and back off. Files: `app/management/commands/send_outbox.py` ```sh python manage.py send_outbox ``` 3. Schedule it and prove idempotence A cron entry every minute. A test that runs the command twice against one pending row and asserts one send. ### Done when - [ ] The request returns without waiting on email - [ ] A failed send is a row with an error, retried on the next run - [ ] Running the command twice sends once - [ ] Attempts are capped so a permanently bad address stops being retried ### Watch out - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ## Not in this build - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. ## After v1, if you want it - A JSON API for the parts a mobile client would need - Full text search using PostgreSQL rather than adding a search service - A staging environment restored from last night's production dump
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. DJANGO_SECRET_KEY=replace-with-50-random-characters # Required. Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. DJANGO_DEBUG=0 # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/appdb # Required. The hostnames this application answers to. Django refuses anything else, which is the point. ALLOWED_HOSTS=example.com,www.example.com # Optional · secret. Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Optional · secret. Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your transactional email provider's SMTP credentials. EMAIL_URL=smtp://user:pass@smtp.provider.com:587
# Bubble · product brief ## Problem A specific Bubble app is often vibecodable now; rebuilding Bubble's editor, workflow engine, plugin ecosystem, hosting, and scaling layer is not. ## Product outcome One real application you own outright, running on your own infrastructure, with payments that reconcile, background work that is visible when it fails, and a restore you have performed rather than assumed. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - web framework - database - auth - hosting - payment/email APIs - product-specific integrations ## Explicit non-goals for v1 - The visual editor, which is the product Bubble actually sells. - The plugin marketplace and the integrations that come with it. - Managed hosting, scaling and the operational cover that goes with a platform. - The ability for a non-developer on your team to change the application without you. - visual editor - workflow builder - plugin marketplace - hosting/scaling - nondeveloper maintenance - templates ## Success criteria - DEBUG is off, ALLOWED_HOSTS is set, and the secret key came from the environment - The permission test that posts to another user's object still passes - A replayed Stripe webhook does not double-fulfil - A restore onto a clean machine has been performed and dated - Every environment variable is documented, and no secret is committed
# Build brief · Bubble 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 the one web application you were going to build in Bubble, written in code with auth, data, workflows and hosting to replace Bubble. 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 clone Bubble. Do not build a visual editor, a drag and drop canvas, a plugin system or a workflow engine. Bubble is worth what it is worth because it is a platform, and a platform is years of work for an audience of one. Build the single application you actually wanted, in code, using a framework that already gives you the parts Bubble was giving you: authentication, a database with migrations, an admin interface and a deploy story. ### Stack (fixed, do not substitute) - Framework: Django 5 with Python 3.12 · authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells - Interface: Django templates with HTMX for partial updates · you get interactive forms and live updates without adopting a separate frontend build and its deployment - Database: PostgreSQL · Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late - Background work: Django management commands driven by cron, moving to a queue only when something actually needs it · most Bubble workflows are a form submission and an email, not a distributed job system - Payments: Stripe Checkout with webhooks · hosted checkout keeps card data off your servers and out of your compliance scope - Hosting: One virtual server behind Caddy, or a managed platform · one server with a documented backup beats an architecture diagram at this size ### Data model (create this before Phase 1) - The worked example is a booking marketplace. Replace these tables with your own domain in Phase 2 and keep the shape. - User · Django's built-in model, extended with a profile rather than replaced - listings(id, owner_id, title, description, price_cents, active, created_at) - availability(id, listing_id, starts_at, ends_at, booked) - bookings(id, listing_id, guest_id, starts_at, ends_at, state, total_cents, stripe_session_id) - messages(id, booking_id, sender_id, body, sent_at) - Every state-changing action is a form POST to a view. That is what a Bubble workflow compiles down to in a code project. ### Phase 1 · Project, accounts and the admin Build: Start a Django project with PostgreSQL configured. Use the built-in authentication views for login, logout and the password reset flow. Configure email so reset actually sends. Create a superuser and confirm the admin is reachable and locked down. This phase is the largest single thing Bubble was giving you for free. Trap: Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. Done when: A new account can be created, logged out of, and logged back into. A password reset email arrives at a real inbox and the link sets a new password. The admin is reachable for a superuser and returns a login page rather than data for anonymous visitors. DEBUG is False in the settings used for anything but local development. Do not build yet: No domain models and no payments. Accounts and the admin only. ### Phase 2 · Your data model and migrations Build: Replace the example models with your own. Put real constraints in the database: foreign keys, not-null, unique together where it matters, and check constraints for things like a positive price. Register the models in the admin so you can create and inspect records before any user-facing screen exists. Trap: Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. Done when: Migrations apply to an empty database and produce the intended schema. Attempting to insert a booking whose end is before its start fails at the database level, not just in the form. The admin lists and creates every model. Running makemigrations again reports no changes, meaning the models and the migrations agree. Do not build yet: No user-facing screens yet. The admin is your interface for now. ### Phase 3 · Screens and workflows Build: Build the list, detail and create screens with Django forms and templates, and use HTMX for the interactions that would otherwise need a page reload. Every workflow is a POST to a view that validates, writes in a transaction, and redirects. Enforce permissions in the view, not in the template. Trap: Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. Done when: A logged-in visitor can complete the main journey from listing to confirmed record without the admin. Posting to another user's object returns a 403 or a 404 rather than succeeding. A double submit of the same form does not create two records, because the write is idempotent or the redirect happens after a successful commit. Do not build yet: No payments and no background jobs. ### Phase 4 · Payments Build: Stripe Checkout for the payment itself, and a webhook endpoint that is the only place an order is marked paid. Verify the webhook signature. Store the Stripe session id on the record and make the handler idempotent, because Stripe will deliver the same event more than once. Trap: Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. Done when: A test-mode payment moves the record to paid only after the webhook arrives. Replaying the same webhook event twice leaves one paid record and does not double-fulfil. A webhook with a bad signature is rejected with a 400. Closing the browser tab immediately after paying still results in a paid record. Do not build yet: No deploy yet; keep using the Stripe test keys. ### Phase 5 · Background work and email Build: Move email sending, reminders and anything slow out of the request. Start with management commands on a schedule, which is enough for most of what a Bubble workflow was doing. Record every send attempt so a failure is a row you can look at rather than a log line you have lost. Trap: Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. Done when: A booking confirmation email is sent by a scheduled command, not during the request, and the request returns in well under a second. A send failure leaves a row marked failed with the error, and a rerun retries only the failed ones. Running the command twice does not send the same email twice. Do not build yet: No deploy hardening yet. ### Phase 6 · Deploy, backups and a restore you have done Build: Deploy behind Caddy with automatic certificates. Serve static files properly. Take a nightly PostgreSQL dump to somewhere off the server, and then restore it onto a second machine to prove the dump is real. Trap: A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed. Done when: The site serves over HTTPS with a valid certificate and DEBUG off. A nightly dump lands off the server. A restore onto a clean machine produces a working application with the same row counts, and the date of that restore is written in the README. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor. It is the product, it is why people pay, and reproducing it is a multi-year project with an audience of one. - The plugin marketplace, which is other people's work and cannot be written into existence. - Bubble's hosting and scaling, which is a real operational service you are now performing yourself. - Letting non-technical colleagues change the application without a developer. That capability is the honest reason many teams stay, and writing the app in code removes it. ### README must contain - Which framework version and which database, and how to run the project locally in under ten minutes - That the webhook is the only thing that marks a payment complete - Every environment variable, what it does, and which ones are secret - The backup schedule, the destination, and the date of the last successful restore - What this application deliberately does not do compared with the Bubble app it replaced
# Architecture · Bubble ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Django 5 with Python 3.12 | authentication, an ORM with migrations, an admin interface and a security posture arrive on day one, which is most of what a no-code platform sells | | Interface | Django templates with HTMX for partial updates | you get interactive forms and live updates without adopting a separate frontend build and its deployment | | Database | PostgreSQL | Bubble is backed by a real database and so should this be; SQLite is fine locally but migrate early rather than late | | Background work | Django management commands driven by cron, moving to a queue only when something actually needs it | most Bubble workflows are a form submission and an email, not a distributed job system | | Payments | Stripe Checkout with webhooks | hosted checkout keeps card data off your servers and out of your compliance scope | | Hosting | One virtual server behind Caddy, or a managed platform | one server with a documented backup beats an architecture diagram at this size | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Auth | Accounts, sessions, password reset. | Django's own. Replace with an external identity provider only if you need single sign-on, and expect that to touch every view that reads the user. | | Domain models | Your tables and the constraints that keep them honest. | This is the part that is genuinely yours. Everything else is replaceable; this is not. | | Views and templates | Screens, forms and authorisation. | A JSON API and a separate frontend is the usual next step. Keep authorisation in the view layer so that swap does not lose it. | | Payments | Checkout sessions and the webhook that is the source of truth. | Another processor is a different session call and a different signature check. The idempotent webhook handler shape survives the change. | | Outbox | Deferred sending with retries and visible failures. | Move to a real queue when latency or retry timing demands it. The outbox table stays useful either way. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DJANGO_SECRET_KEY` · required, secret · Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. - `DJANGO_DEBUG` · required · Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `ALLOWED_HOSTS` · required · The hostnames this application answers to. Django refuses anything else, which is the point. - `STRIPE_SECRET_KEY` · optional, secret · Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. - `STRIPE_WEBHOOK_SECRET` · optional, secret · Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. - `EMAIL_URL` · required, secret · Your transactional email provider's SMTP credentials. ## 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 · Bubble product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Django 5 with Python 3.12, Django templates with HTMX for partial updates, PostgreSQL, Django management commands driven by cron, moving to a queue only when something actually needs it, Stripe Checkout with webhooks, One virtual server behind Caddy, or a managed platform. - 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 - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. - A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed.
# Delivery milestones · Bubble Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Project, accounts and the admin Signup, login, logout and password reset all work, and you can edit data through the admin, before you write a single feature. ### Steps 1. Create the project and point it at PostgreSQL Start the project, add psycopg, and set the database from environment variables. Never commit a settings file with credentials in it. Files: `config/settings.py`, `.env.example` ```sh python -m venv .venv pip install django psycopg[binary] python-dotenv django-admin startproject config . python manage.py migrate ``` 2. Wire the built-in auth views and a real email backend Include django.contrib.auth.urls, add templates for login and the reset flow, and configure SMTP against a transactional provider. Send yourself a reset to prove it. Files: `templates/registration/` ```sh python manage.py createsuperuser ``` 3. Lock the admin down before you forget Move it off /admin, require staff status, and confirm an anonymous request gets a login page. Turn DEBUG off outside local development and set ALLOWED_HOSTS. Files: `config/urls.py` ### Done when - [ ] Signup, logout and login all succeed - [ ] A password reset email reaches a real inbox and the link works - [ ] An anonymous request to the admin returns a login page, not data - [ ] The application refuses to start with DEBUG True when the environment says production ### Watch out - Django's password reset does nothing visible without a working email backend, and the development console backend makes it look like it worked. Point it at a real transactional provider now, or you will discover this the week you launch. ## M2 · Your data model and migrations Your real tables exist, with constraints the database enforces rather than the form. ### Steps 1. Write the models with database constraints Define the models and add Meta.constraints for the invariants. A CheckConstraint for a positive price and an ordered date range costs three lines and holds forever. Files: `app/models.py` ```sh python manage.py makemigrations python manage.py migrate ``` 2. Register everything in the admin list_display, search_fields and list_filter for each model. This is the closest analogue to Bubble's data tab and it is worth ten minutes. Files: `app/admin.py` 3. Seed realistic data A management command that creates a few users, listings and bookings so every later screen has something to render and every query has rows to be slow against. Files: `app/management/commands/seed.py` ```sh python manage.py seed ``` ### Done when - [ ] Migrations apply cleanly to an empty database - [ ] A booking with an end before its start is rejected by the database - [ ] makemigrations reports no pending changes after migrate - [ ] The seed command populates every model and is safe to run twice ### Watch out - Validating only in the form leaves the database willing to hold nonsense the moment anything else writes to it, including a management command or the admin. Put the rule in a constraint and let the form report the violation. ## M3 · Screens and workflows A visitor can complete the main journey end to end without you touching the admin. ### Steps 1. Build the core screens with forms List, detail and create. Use ModelForm so validation lives with the model. Keep templates thin. Files: `app/views.py`, `templates/` 2. Add HTMX for the interactive parts Load HTMX from a vendored file rather than a content delivery network, and return HTML fragments from views. No build step, no bundler. Files: `static/htmx.min.js` 3. Authorise in the view and test it A test that logs in as user B and posts to user A's object, expecting a refusal. This is the test that catches the mistake everyone makes. Files: `app/tests/test_permissions.py` ```sh python manage.py test ``` ### Done when - [ ] The main journey completes without touching the admin - [ ] Posting to another user's object is refused - [ ] A double form submit does not create two records - [ ] Every form shows field errors rather than a server error page on bad input ### Watch out - Hiding a button in a template is not authorisation. Anyone can post to the URL. Check ownership and permission inside the view, and write a test that posts to another user's object and expects a refusal. ## M4 · Payments Money moves, and your database agrees with Stripe about what happened. ### Steps 1. Create a Checkout session server side Never trust a price from the client. Look the amount up from your own database and pass it to Stripe. Files: `app/payments.py` ```sh pip install stripe ``` 2. Handle the webhook as the only source of truth Verify the signature with the endpoint signing secret, then update state. Exempt this view from CSRF, because it is a server to server call, and rely on the signature instead. Files: `app/webhooks.py` 3. Make the handler idempotent and test replay Store the processed event id and ignore duplicates. Use the Stripe command line tool to replay an event and assert nothing changes the second time. ```sh stripe listen --forward-to localhost:8000/webhooks/stripe/ stripe trigger checkout.session.completed ``` ### Done when - [ ] A test payment marks the record paid only after the webhook - [ ] Replaying the same event leaves exactly one paid record - [ ] A bad signature is rejected with a 400 - [ ] Closing the tab after payment still results in a paid record ### Watch out - Marking the order paid on the success redirect is the classic and expensive mistake. The visitor can close the tab, and the redirect can be replayed. The webhook is the source of truth and the redirect is only a nice screen. ## M5 · Background work and email The things that should not block a request stop blocking it, and failures are visible. ### Steps 1. Add an outbox table Rows with state, attempts and last error. Views write rows; the command sends them. This is what makes retries and visibility possible. Files: `app/models.py` 2. Write the send command Select pending rows, send, mark sent or failed with the error text. Cap attempts and back off. Files: `app/management/commands/send_outbox.py` ```sh python manage.py send_outbox ``` 3. Schedule it and prove idempotence A cron entry every minute. A test that runs the command twice against one pending row and asserts one send. ### Done when - [ ] The request returns without waiting on email - [ ] A failed send is a row with an error, retried on the next run - [ ] Running the command twice sends once - [ ] Attempts are capped so a permanently bad address stops being retried ### Watch out - Reaching for Celery and Redis on day one adds two services to operate for work that a cron entry handles at this size. Add the queue when a command is too slow or needs retries within seconds, and not before. ## M6 · Deploy, backups and a restore you have done (production only) It runs somewhere other than your laptop, and you have proved you can bring it back. ### Steps 1. Deploy behind Caddy with a real certificate Gunicorn behind Caddy, static files collected and served by Caddy, secrets from the environment, DEBUG off and ALLOWED_HOSTS set. Files: `Caddyfile` ```sh python manage.py collectstatic ``` 2. Automate an off-server dump pg_dump on a schedule, encrypted or to a destination only you control, keeping several generations. ```sh pg_dump "$DATABASE_URL" | gzip > backup-$(date +%F).sql.gz ``` 3. Perform the restore drill and date it Restore the newest dump onto a second machine, run migrations, and compare row counts. Write the date in the README. Files: `README.md` ```sh gunzip -c backup.sql.gz | psql "$RESTORE_URL" ``` ### Done when - [ ] The site serves over HTTPS with a valid certificate - [ ] DEBUG is off and the admin is not at the default path - [ ] A dump exists off the server and can be listed from another machine - [ ] A restore was performed on a clean machine and the date is recorded ### Watch out - A backup on the same server is not a backup; it dies with the disk it lives on. Get the dump off the machine, and confirm you can read it from wherever it landed.
# Operations · Bubble ## Backup Nightly pg_dump to a destination off the server, several generations retained. Uploaded files, if any, need their own copy; a database dump does not contain them. ## Restore Create a database, load the newest dump, run migrations, point the application at it, and compare row counts against what the dump claimed. Do this on a schedule, not only in an emergency. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring An uptime check against a health endpoint that touches the database, plus an alert if the outbox has rows older than an hour, which is how you learn email broke without reading logs. ## Incident checklist Keep the previous release deployable. For a bad migration, restore the dump taken immediately before it rather than writing a reverse migration under pressure. For a payment dispute, the webhook event log is your record of what actually happened. 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 - [ ] DEBUG is off, ALLOWED_HOSTS is set, and the secret key came from the environment - [ ] The permission test that posts to another user's object still passes - [ ] A replayed Stripe webhook does not double-fulfil - [ ] A restore onto a clean machine has been performed and dated - [ ] Every environment variable is documented, and no secret is committed ## Launch constraint Do not market omitted Bubble 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 · secret. Generate once with get_random_secret_key(). Changing it invalidates every session and signed value. DJANGO_SECRET_KEY=replace-with-50-random-characters # Required. Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors. DJANGO_DEBUG=0 # Required · secret. Your PostgreSQL connection string. DATABASE_URL=postgres://user:pass@localhost:5432/appdb # Required. The hostnames this application answers to. Django refuses anything else, which is the point. ALLOWED_HOSTS=example.com,www.example.com # Optional · secret. Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done. STRIPE_SECRET_KEY=sk_test_... # Optional · secret. Printed by stripe listen during development, or shown when you register the endpoint in the dashboard. STRIPE_WEBHOOK_SECRET=whsec_... # Required · secret. Your transactional email provider's SMTP credentials. EMAIL_URL=smtp://user:pass@smtp.provider.com:587
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because future changes can be made visually without owning a codebase.
xvisual editor
xworkflow builder
xplugin marketplace
xhosting/scaling
xnondeveloper maintenance
xtemplates
Don't feel like building it? These folks already made it free.
all 5 free alternatives to Bubble →· no votes, no pay-to-list · just what's real
Bubble pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free, web & mobile | $0/workspace | $0/workspace | 1 editor; 50,000 workload units/month; 6 hours server logs; development version only |
| starter, web & mobile | — | $59/workspace | 1 editor; 175,000 workload units/month; 2 days logs; 5 mobile build submissions/month; 3 live app versions |
| growth, web & mobile | — | $209/workspace | 2 editors; 250,000 workload units/month; 14 days logs; 10 branches; 10 build submissions; 5 live versions |
| team, web & mobile | — | $549/workspace | 5 editors; 500,000 workload units/month; 20 days logs; 25 branches; 20 build submissions; 8 live versions |
| enterprise | custom | — | Custom workload, hosting region, server, build and support limits |
free tier1 editor; 50,000 workload units/month; 6 hours of server logs; development-only web/mobile versions with no live deployment
billingmonthly + annual (save up to 20%); each project needs its own plan
hidden costsWorkload units are the real meter and web + mobile share one allowance; overage or prepaid workload tiers cost extra, storage is $3 per additional 100 GB/month, and paid plugin subscriptions are separate
verified 2026-08-14 · source ↗
Vibecode Bubble
Kinda. The core of Bubble is buildable in a weekend with the prompt on this page, but there are real gaps: visual editor, workflow builder. Read the honest list above before committing.
How much does Bubble cost?
Bubble costs about $59/month (Starter, Web & Mobile, checked 2026-08-14), which is $708 per year.
What do I lose by replacing Bubble?
Honestly: visual editor; workflow builder; plugin marketplace; hosting/scaling; nondeveloper maintenance; templates. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Bubble?
Yes: Budibase (Custom web apps with data, forms, screens, and automations; design freedom is far below Bubble's.) NocoBase (A full database-backed web-app builder with pages, roles, actions, workflows, and plugins; more business app than consumer startup.) Corteza (A self-hosted business-app suite with records, pages, workflows, charts, and enterprise-grade amounts of configuration.) All 5 curated free alternatives are at vibecodeit.com/bubble/alternatives. The prompt is for when you want it exactly your way.