Build Bubble

KINDAreplaces $59/mosaves $708/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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-daythe files for this build are in the project pack

FrameworkDjango 5 with Python 3.12InterfaceDjango templates with HTMX for partial updatesDatabasePostgreSQLBackground workDjango management commands driven by cron, moving to a queue only when something actually needs itPaymentsStripe Checkout with webhooksHostingOne virtual server behind Caddy, or a managed platform

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    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. open ↗

    Verify python3 --version prints 3.12 or higher

  2. installfree

    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. open ↗

    Verify psql --version prints 16 or higher and you can connect to your project database

  3. API keyfree 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. open ↗

    Verify stripe listen prints a webhook signing secret beginning whsec_

  4. API keymost 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

  5. have readyroughly 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.

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DJANGO_SECRET_KEYsecretrequiredreplace-with-50-random-charactersGenerate once with get_random_secret_key(). Changing it invalidates every session and signed value.
DJANGO_DEBUGrequired0Zero everywhere except your own machine. Debug mode leaks settings and stack traces to visitors.
DATABASE_URLsecretrequiredpostgres://user:pass@localhost:5432/appdbYour PostgreSQL connection string.
ALLOWED_HOSTSrequiredexample.com,www.example.comThe hostnames this application answers to. Django refuses anything else, which is the point.
STRIPE_SECRET_KEYsecretoptionalsk_test_...Stripe dashboard, Developers > API keys. Use the test key until the restore drill has been done.
STRIPE_WEBHOOK_SECRETsecretoptionalwhsec_...Printed by stripe listen during development, or shown when you register the endpoint in the dashboard.
EMAIL_URLsecretrequiredsmtp://user:pass@smtp.provider.com:587Your transactional email provider's SMTP credentials.

The build, in order

  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.

    1. 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

      terminal
      python -m venv .venv
      pip install django psycopg[binary] python-dotenv
      django-admin startproject config .
      python manage.py migrate
    2. 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/

      terminal
      python manage.py createsuperuser
    3. 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 · tick each as it passes
    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.
  2. Your data model and migrations

    Your real tables exist, with constraints the database enforces rather than the form.

    1. 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

      terminal
      python manage.py makemigrations
      python manage.py migrate
    2. 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. 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

      terminal
      python manage.py seed
    done when · tick each as it passes
    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.
  3. Screens and workflows

    A visitor can complete the main journey end to end without you touching the admin.

    1. List, detail and create. Use ModelForm so validation lives with the model. Keep templates thin.

      Files app/views.pytemplates/

    2. 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. 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

      terminal
      python manage.py test
    done when · tick each as it passes
    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.
  4. Payments

    Money moves, and your database agrees with Stripe about what happened.

    1. Never trust a price from the client. Look the amount up from your own database and pass it to Stripe.

      Files app/payments.py

      terminal
      pip install stripe
    2. 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. 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.

      terminal
      stripe listen --forward-to localhost:8000/webhooks/stripe/
      stripe trigger checkout.session.completed
    done when · tick each as it passes
    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.
  5. Background work and email

    The things that should not block a request stop blocking it, and failures are visible.

    1. 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. Select pending rows, send, mark sent or failed with the error text. Cap attempts and back off.

      Files app/management/commands/send_outbox.py

      terminal
      python manage.py send_outbox
    3. A cron entry every minute. A test that runs the command twice against one pending row and asserts one send.

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more no-code apps.