django-mvp-payments
Payment and subscription UI as Cotton components for django-mvp projects, one namespace per payment backend.
It covers pricing, checkout and subscriptions for django-mvp projects.
Django has no shortage of packages that talk to a payment provider. What none of them give you is the part your users look at. A package hands you a subscription record, a list of prices and an endpoint that starts a checkout, and leaves the plan grid, the subscribe button, the billing panel and the "your card expires next month" notice to be built from raw utility classes — in every project, every time.
This package is that layer, plus the least it takes to keep it fed. It runs no payment logic and
depends on no third-party payment backend. For Stripe it has an app of its own,
mvp_payments.stripe, which keeps a small copy of each user's subscription in step with Stripe
through webhooks. Its pages turn up in django-mvp's Account Center, rather than leaving you to
build and route them.
Status
Version 0.0.1. The stripe namespace contributes one entry to the Account Center, Subscription,
under a Billing group. That page shows a signed-in person every subscription they currently hold,
the way to switch plans, and the way through to Stripe's billing portal. The plans page it leads
to is a public page of its own. It draws every recurring price in your Stripe account, with each
product's name, description and features, and says so plainly when Stripe cannot be reached or has
nothing on offer. Nothing here is stable.
Requirements
- Python 3.12+
- Django 5.2, 6.0 or 6.1
- django-mvp 0.30.0+
- For Stripe, the
stripe-liteextra and a Stripe account
Install
pip install django-mvp-payments
Add it to INSTALLED_APPS, before mvp:
INSTALLED_APPS = [
...,
"mvp_payments",
"mvp",
]
That order is load-bearing, not a style choice: this package ships its own copy of
mvp/account/overview.html and extends the name from inside it, which only resolves when this
application is found first.
Then mount its URLs wherever you like:
urlpatterns = [
...,
path("account/billing/", include("mvp_payments.urls")),
]
Mount it where a reader would expect to find it. These are Account Center pages, so inside the
Account Center's own prefix and under the label the navigation gives them is the natural place.
Addresses carry no backend name: a page is at <your prefix>/subscription/, never
<your prefix>/stripe/subscription/, because which library you chose to talk to your
payment provider is not a person's business while they read their own subscription.
To offer the plans page, add a second include at the address you choose, outside the Account Center:
urlpatterns = [
...,
path("plans/", include("mvp_payments.plans_urls")),
]
Those two lines are the only wiring. From there, every backend you have installed contributes its own
section of the Account Center's navigation and its own card to the Account Center's overview, and a
backend you have not installed contributes nothing. There is no settings block, no flag to turn on
and no registry to populate — what is in INSTALLED_APPS decides what exists.
The Stripe app
To keep each user's Stripe subscription in your database, install the extra and add the app:
pip install "django-mvp-payments[stripe-lite]"
INSTALLED_APPS = [
...,
"mvp_payments",
"mvp_payments.stripe",
"mvp",
]
Then set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in MVP_PAYMENTS and run
python manage.py migrate.
How to install the Stripe app
has the steps, how to point Stripe at the webhook
the rest of the setup, and the
Stripe app reference
the settings, the records and the webhook.
Namespaces
Components are grouped by the backend they speak to, and the namespace is that backend's name:
<c-stripe.plans :plans="plans" />
<c-stripe.subscription :subscription="subscription" />
Each namespace assumes one backend's records and nothing else. Two namespaces never share markup or a data shape, because the things they are describing are not the same thing wearing different names — a subscription in one library is a different record with different fields from a subscription in the next.
Shipped today: stripe, built on the package's own Stripe app. It contributes a user subscriptions page
to the Account Center. That page lists every subscription a person currently holds, reading the Stripe app's
records, and hands the reader to Stripe's own billing portal and plan-change screen through views the
app mounts. It also carries the way to the plans page, a public page of its own that draws every
price on offer in your Stripe account and that a project mounts with mvp_payments.plans_urls. Namespaces for other backends are welcome and do not need this one's
agreement about anything: adding one is adding it beside the ones already installed, and changes
nothing about their navigation entries, their card or their pages.
docs/reference/user-subscriptions-page.md covers the user subscriptions page: what it puts in the template context, the components it is built from, and how to replace it with your own.
docs/reference/plans-page.md covers the plans page: where to mount it, the parts
it is built from and the data each receives, the setting it reads, the {% stripe_plans %} tag,
and how to place the plans on your own pages or replace one part.
docs/namespaces.md is how you add a namespace: what one declares, the two questions it answers about itself, and what it may not do.
JavaScript
No component here needs a payment provider's script. The plans are drawn on the server, and checkout, the billing portal and plan changes all happen on Stripe's own pages. This package never emits a script tag for a provider's library, and should a future component need one, loading it is your project's decision.
The one script of its own, mvp_payments/stripe/billing_portal.js, binds the "Manage
subscription" and "Switch plans" controls. Include it with
{% static %} on any page that shows those controls, as
the user subscriptions page reference shows.
Scope & philosophy
What this is. A presentation layer for payment and subscription state, and, for each
supported provider, a small app that keeps that state. Templates, the small amount of JavaScript
some components need, the Python that puts a page at a URL and an entry in a menu, and for
Stripe, mvp_payments.stripe.
The pages look like the rest of your django-mvp site, because they are drawn with the same daisyUI classes and follow the same theme. They show a person where they stand. Anything that changes what they pay, such as choosing a plan, switching, cancelling or updating a card, is handed to the provider's own hosted pages, which already do it well.
The Stripe app keeps the least it can: who each user's Stripe customer is, and that customer's
subscription with its status, price and period. Stripe is the source of truth. The app learns of
changes from Stripe's webhooks, checks each one's signature and then asks Stripe for the current
state, rather than trusting what the webhook says. It is installed with
django-mvp-payments[stripe-lite]
(ADR 0010).
The standing directions this package works toward are in GOALS.md.
What this deliberately is not.
- Not a full Stripe integration. No copy of Stripe's products, prices, invoices or payment methods, no taxes, no metered billing, no one-off payments. If you need those, dj-stripe is built for it.
- Not a decider. Nothing here computes a price, a proration or a trial, and nothing decides what a subscription grants. The Stripe app can tell your views whether someone is subscribed. What that lets them see is your project's call, enforced in your views.
- Not a place card details pass through. Cards are typed into the provider's own pages and iframes, never into markup written here.
- Not a way to run two backends at once. Whose subscription a person is looking at stops being answerable, and nothing here is built to arbitrate it.
- Not a backend abstraction. There is no common interface that every payment provider gets adapted onto. That abstraction is the classic trap in this domain: it fits the first provider, bends for the second and is a liability by the third, because subscription models genuinely differ. A namespace per backend costs some duplication and buys the freedom to match each one exactly.
- Not a CSS framework. Components render the daisyUI classes django-mvp already ships. No stylesheet, no build step, no theme of its own.
Tie-breaks. When two of these pull against each other:
- match the provider rather than generalise
- keep less rather than more, and ask the provider rather than guess
- route and render rather than compute
- render less rather than assume how a page is laid out
Running the demo
uv sync
uv run python manage.py migrate
uv run python manage.py seed_demo
uv run python manage.py runserver 8020
Sign in as regular.user, staff.user or super.user, password password. Out of the box the
pages run on fake Stripe values, so the billing portal says it could not be reached, while
seed_demo puts a demo set of plans (Basic, Pro and Team, monthly and yearly) in the demo's file
cache for the plans page. To see the portal and your own plans working, copy demo/.env.example to demo/.env and fill it in from your own
Stripe sandbox. That file is gitignored. Then run seed_demo again so the demo accounts get real
sandbox customers and subscriptions, recorded by the Stripe app as well, where super.user can
look at them in the admin at /admin/. The file lists the one portal setting "Switch plans" needs
from your sandbox.
To have the demo record subscriptions made in the sandbox, forward Stripe's webhooks to it with the Stripe CLI in a second terminal:
stripe listen --forward-to localhost:8020/account/billing/stripe/webhook/ \
--events checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,customer.subscription.paused,customer.subscription.resumed,customer.deleted,product.created,product.updated,product.deleted,price.created,price.updated,price.deleted
It prints a whsec_... signing secret. Put it in demo/.env as STRIPE_TEST_WEBHOOK_SECRET and
restart the demo. Subscribing, switching plans and cancelling in the sandbox then show up in the
demo's records. The demo starts without the command
running, and python manage.py stripe_sync brings the records up to date after a gap.
Contributing
Changes follow CONSTITUTION.md, the testing standards, the code documentation standards and the Django standards.
Changelog
See CHANGELOG.md.
Licence
MIT.
Metadata
Release files for django-mvp-payments 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| django_mvp_payments-0.1.0.tar.gz | 39.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_mvp_payments-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 96.0 kB
Release files / django_mvp_payments-0.1.0.tar.gz
| Download URL | django_mvp_payments-0.1.0.tar.gz |
|---|---|
| Size | 39.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
957085db2776e1be5632b0333b17472ed74ed4adb2acebbd97eabfd62feb840a
|
|
BLAKE2b-256 checksum How to use checksums |
16d7bc8e3a4ba057b9e8e4bc77deb6ee3bcac0b447498f5700013e06c011ef49
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / django_mvp_payments-0.1.0-py3-none-any.whl
| Download URL | django_mvp_payments-0.1.0-py3-none-any.whl |
|---|---|
| Size | 56.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e6672094dc1ff25f8ad269b14777e259815124ad23e7fd9d787103e9230a4d33
|
|
BLAKE2b-256 checksum How to use checksums |
8b5acacbc2ef53c331d865ff846bfd696ad9cf04b683f172f23c84ec76459a41
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for django-mvp-payments 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for django_mvp_payments-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.