Build secure, subscription-based e-learning platforms with Webflow CMS and Vimeo’s secure video delivery.
Webflow takes care of the design and hosting, while Authorization0 manages login, Stripe handles subscriptions and a lightweight serverless worker keeps membership status synchronized via Striped web hooks. Gating logic on the front end checks both login and an active subscription flag before displaying protected content.
Note: An alternative to the tailor-made solution suggested in this guide is to use an off-the-shelf tool such as Outseta or MemberStack. Although less customizable, Outseta gives you built-in authentication, payments, secure content, CRM, email, and helpdesk capabilities right in Webflow Designer. Meanwhile, MemberStack offers flexible no-code access and Stripe-powered membership control using data attributes, perfect for quick configurations without a backend layer.
System architecture
The architecture retains Webflow for content design and delivery, while delegating identity, billing, and subscription status to external services. A small collaborator bridges Stripe and your authentication layer, allowing the site to gate content based on subscription status.
- Webflow frontend – Host public pages (Home, Pricing, Login) and members-only pages (Trainings, Programs, Dashboard). Authentication and porting are performed via custom JS embedded in the site.
- Authentication – Auth0 authentication manages signup/login and issues JSON Web Tokens (JWTs). A custom active flag in the user profile or JWT claims indicates whether a subscription has been paid.
- Billing – Stripe provides subscription products, recurring payments, payment links before checkout, and provides a Customer portal for self-service account management.
- Employee -A serverless function receives and verifies Striped web hookssuch as checkout.session.completed or customer.subscription.updated. It also updates the active flag in the datastore and saves assignments such as
user_id,stripe_customer_idand subscription status. - Data archive (employee) – Optional store (Firestore, KV) for assigning users to Stripe clients and recording last processed event IDs for idempotence.
- Redirects and environments – Stripe success URL →
/dashboardwhile canceling URL → /prices. Use separate Stripe keys, webhook endpoints, and datastore configurations for development and production.
Authenticate users and track membership status
Before you can decide whether to display a training or a paywall, you need to know two things: who the user is and whether they have paid.
We use Auth0 authentication for both. It issues a WT on login and can store an active flag in a custom claim or Firestore record. This keeps access control local in the browser, fast and independent of Stripe’s API.
Start by loading the site-wide Auth0 SDK using Webflow’s custom code feature.
On the front end, each secure page performs a check:
document.addEventListener("DOMContentLoaded", async () => {
// Initialize Auth0 SPA client
const auth0Client = await createAuth0Client({
domain: "YOUR_DOMAIN",
clientId: "YOUR_CLIENT_ID",
authorizationParams: {
redirect_uri: window.location.origin
}
});
From there:
- Log in and log out – Provide a form or modal that calls
auth0Client.loginWithRedirect()for logging in, andauth0Client.logout()to log out. Add the ‘Log in’, ‘Log out’ and ‘Manage subscription’ links to your navigation system. - Identification information – Each user must provide a uid, email address and, after their first purchase, a
stripe_customer_id. The active flag lives next to this one. - Mapping to Stripe – Allow Stripe to create the customer record during checkout. When your employee receives the checkout.session.completed event, write the customer ID back to the datastore profile.
- Email synchronization – If a user changes their email address in the datastore, update Stripe with the update customer API so the Customer portal logging in works without any hitches.
On the front end, each secure page performs a check:
let token;
try {
token = await auth0Client.getTokenSilently();
} catch (err) {
// No JWT → force login page
window.location.href = "/login";
return;
}
const payload = JSON.parse(atob(token.split(".")[1]));
const isActive = payload["https://yourapp.com/active"];
if (!token) {
// Case 1: No JWT at all
window.location.href = "/login";
} else if (isActive === true) {
// Case 2: Paid user → Allow page to load
document.body.style.display = "block";
} else {
// Case 3: JWT exists but subscription inactive
window.location.href = "/pricing";
}
});
- No JWT? Send them to /login.
- JWT present with active=true? Show content.
- JWT present but inactive? Send to /prices.
For additional security, add a “Refresh Access” button that calls a worker endpoint to retrieve subscription status from Stripe and update claims.
Subscriptions with Stripe payment links and portal
Stripe is the source of truth for subscription status. Your site never processes card data, but simply directs users to Stripe for payment and self-service.
This is the current you will set:
- Create a subscription product in your Striped dashboardsuch as ‘Coaching monthly’, with a recurring payment.
- Generate one Payment link for that product. Set the success URL to
/dashboardand the cancellation URL to/pricing. Add query parameters forprefilled_email,locked_prefilled_email=trueAndclient_reference_id(set to the user’s datastore UID). This makes checkout smoother and ensures your webhook knows which user to activate.
When users need to manage their subscription, give them access to the Stripe Customer Portal. The no-code option sends an email with a one-time passcode; The API option allows your employee to create a portal session directly, so logged in users skip that step. Configure the portal to allow the actions you want (cancellations, plan changes, map updates) and brand it with your logo.
Webhooks are where the pieces connect:
- checkout.session.completed → mark active=true, save
stripe_customer_idin data storage. - customer.subscription.updated → enable active based on Stripe status.
- customer.subscription.deleted → set
active=false. - invoice.payment_failed → immediately mark inactive or treat it as one
past_duestands.
In this section we only map the most important events. Later, in the workers section, we’ll see how to build the serverless function that listens to these webhooks and updates the datastore so that your site responds quickly without polling.
Enforce access rules for pages, assets, and media
Your identity and billing let you decide who sees what. The safest standard is: hide everything until proven otherwise.
At the page levelperform your auth + active check when loading the page. If the user fails, redirect him to /login or /pricing before any secure HTML is exposed. This prevents casual browsing, but remember that Webflow pages are static (the HTML is submitted before your JavaScript runs), so keep sensitive items (like videos) out of the page source.
For control at element levelyou can structure public pages with teaser content and premium content side by side. Use classes or data attributes to specify what should be shown only to active members, then enable visibility in your script.
Media deserves extra attention. Host premium videos on a service that supports short-lived signed URLs such as AWS CloudFront signed URLs or Firebase Storage with security rules. Generate a URL only after confirming the user’s active flag. This way, even if someone takes the URL from the network tab, it won’t work for long.
Clearly define your access statuses:
- Active → full access
- Overdue → block access or show the message “update payment”.
- Cancelled → block access completely
Sync status with employees
The employee is the translator of the system. It listens to Stripe, updates the datastore, and makes sure both agree on who is active.
Implement it in a lightweight runtime like Webflow cloudVercel Functions or AWS Lambda. You need:
- Secrets – Stripe webhook signing secret and datastore credentials in environment variables.
- Verification – Usage
stripe.webhooks.constructEventUnpleasant confirm signatures. - Mapping – Prefer
client_reference_idfrom the event to map to datastore uid; fall back on email if this is lacking. - Updates – Use the Auth0 SDK to set the active flag to true in a custom claim or to update your datastore. Save the
stripe_customer_idif it is new. - Idempotency – Log processed event.id values ​​to avoid duplicate updates.
- Performance – Respond 2xx quickly; perform heavier processing asynchronously. Stripe will try again for non-200 responses.
- /endpoint recheck – Optional endpoint that the front end can click to refresh status on demand.
Conclusion and next steps
Check in webhook deliveries the Stripe dashboard so you know if an event has failed and needs to be resolved.
Simply put: preserve Webstream For the UI, perform authentication and billing through managed services, and have a lightweight worker keep the active flag in sync.
In a development environment, start with Stripe test modeverifying the entire flow; login, checkout, portal access and webhook updates before going live. Hide member content by default and only reveal it after both JWT and active checks pass.
For billing, Stripe payment links and the Customer portal can be implemented quickly; you can always add usage-based products or multi-subscription tiers later.
Operationally monitor webhook logins Striped dashboardhandle errors quickly and consider a ‘Refresh Access’ button in the dashboard for edge cases. Define a clear UX for past and canceled statuses so members know how to regain access.
Further reading:
#Build #subscription #membership #site #Stripe #Auth0 #Webflow #Cloud #Webflow #blog


