⏱️ TL;DR
- If your site is plain HTML with no build step, do not run npm install @memberstack/dom. That package has no browser build, so there is no URL a <script> tag can point at. This is the single most common reason a custom code install stalls.
- Install the same script Webflow uses. It is not Webflow specific. It runs on any HTML page you can add a script tag to.
- That script is the DOM package with a data attribute layer on top. One script tag gives you both the data-ms-* attributes and the full JavaScript API at window.$memberstackDom. You do not have to choose.
- Add your domain in the dashboard. If the domain is not registered, the script runs in Test Mode, which is the usual reason live signups never appear.
1. Why the npm instructions do not work on a plain HTML site
When you create a Custom Code app, the dashboard tells you to install our package from npm:
npm install @memberstack/dom
That instruction is correct, but only for a certain kind of project. It assumes you have a build step: a bundler like Vite, webpack, or Next.js that reads your import statements and compiles everything into files a browser can run. React, Next.js, Vue, Svelte, and SvelteKit projects all work this way.
If your site is a folder of .html files that you edit and upload, you have no build step, and the npm instructions have nowhere to land.
It is worth being precise about why, because the usual next guess does not work either. People reasonably try to load the package straight from a CDN:
<!-- This will not work -->
<script src="https://unpkg.com/@memberstack/dom"></script>
@memberstack/dom only publishes module builds meant for a bundler to consume. It does not publish a standalone browser bundle, so there is no file at that address for a script tag to run. This is a property of the package itself, not something a different CDN or URL will solve.
The fix is not to force npm to work. It is to install Memberstack a different way.
2. The part that makes this click
Memberstack has a second install: a single hosted script, historically called the Webflow package. The name causes most of the confusion here, so here is what it actually is.
The Webflow script is the DOM package, wrapped. Internally it imports @memberstack/dom, calls init() for you using the app ID on your script tag, and adds a layer that scans your page for data-ms-* attributes and wires up forms, buttons, and content gating automatically.
Two consequences follow, and they are the whole point of this article:
- Nothing in that script requires Webflow. It reads HTML attributes and talks to our API. A hand written HTML page is a perfectly normal place to run it.
- You still get the full JavaScript API. Because calling init() is exactly what creates window.$memberstackDom, that global exists on any page where the script loaded. Every method documented for the DOM package is available on it.
So these are not two competing products with different feature sets. They are one library with two ways to drive it, and the script tag gives you both at once: attributes for the common things, JavaScript for everything else.
3. Install the script
Paste this into the <head> of every page, before any other script that uses Memberstack:
<script
data-memberstack-app="YOUR_APP_ID"
src="https://static.memberstack.com/scripts/v2/memberstack.js"
type="text/javascript">
</script>
Replace YOUR_APP_ID with your own app ID, which starts with app_. You can copy the whole tag, already filled in, from Settings > Install in your Memberstack dashboard, or from the Getting Started page.
That is the entire installation. There is nothing to build and nothing to deploy.
4. Option A: use data attributes and write no JavaScript
With the script installed, you can build most of a membership site by adding attributes to ordinary HTML. The script finds these elements and handles the rest.
A signup form:
<form data-ms-form="signup">
<input data-ms-member="email" type="email" required />
<input data-ms-member="password" type="password" required />
<button type="submit">Sign up</button>
</form>
A login form is the same shape with data-ms-form="login".
Show and hide content based on who is logged in:
<div data-ms-content="members">Welcome back.</div>
<div data-ms-content="!members"><a href="/login">Log in</a> to continue.</div>
<!-- Gate content behind a specific plan -->
<div data-ms-content="premium">Premium content here.</div>
Buttons and member data:
<button data-ms-modal="signup">Sign up</button>
<button data-ms-action="logout">Log out</button>
<span data-ms-member="email"></span>
The full list, including checkout buttons, passwordless login, profile forms, and social providers, is in All Webflow Package Data Attributes. Every attribute in that article works on a plain HTML page.
5. Option B: use JavaScript through window.$memberstackDom
When the attributes do not cover what you need, reach for the JavaScript API on the same install. Assign it to a variable and call methods on it:
const memberstack = window.$memberstackDom;
// Who is logged in?
const { data: member } = await memberstack.getCurrentMember();
// Log someone out
await memberstack.logout();
// React to login and logout as it happens
memberstack.onAuthChange((member) => {
if (member) {
// signed in
} else {
// signed out
}
});
A complete custom signup form, handled entirely in JavaScript rather than with data-ms-form:
<form id="signup-form">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign up</button>
</form>
<script>
function initSignup() {
const memberstack = window.$memberstackDom;
document.getElementById("signup-form").addEventListener("submit", (event) => {
event.preventDefault();
memberstack
.signupMemberEmailPassword({
email: event.target.email.value,
password: event.target.password.value,
})
.then(() => {
window.location.href = "/onboarding";
})
.catch((error) => {
console.error(error);
});
});
}
// See section 6 for why this guard is here
if (window.$memberstackReady) {
initSignup();
} else {
document.addEventListener("memberstack.ready", initSignup);
}
</script>
You can mix both options freely on the same page. Using data-ms-form="login" for your login page and custom JavaScript for a dashboard widget is a normal thing to do.
6. Wait for Memberstack before calling methods
The most common error on a script tag install is this one:
Uncaught TypeError: Cannot read properties of undefined (reading 'getCurrentMember')
It means your code ran before the Memberstack script finished loading, so window.$memberstackDom did not exist yet.
Memberstack signals readiness in two ways: it sets window.$memberstackReady to true, and it fires a memberstack.ready event on document. Check both. Listening for the event alone is a real bug: if Memberstack finished loading before your listener was attached, the event has already fired and your callback will never run.
This is the pattern to use:
function runMyCode() {
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember().then(({ data: member }) => {
if (member) {
console.log("Logged in as", member.auth.email);
}
});
}
if (window.$memberstackReady) {
runMyCode();
} else {
document.addEventListener("memberstack.ready", runMyCode);
}
There is a longer walkthrough in Ensuring Memberstack Initialization for Reliable Method Access. It is written for Webflow, but the mechanism is identical here.
7. Add your domain, or you will stay in Test Mode
This one catches people out, so it is worth understanding rather than just following.
When the script tag carries only data-memberstack-app, our API works out which mode to use by looking at the domain the request came from. If that domain is registered in your dashboard, it uses the mode you assigned to it. If the domain is not registered, the request falls back to Test Mode.
So a site published to a host you have not told Memberstack about will happily create members, and they will all be test members. Everything looks like it works, but your live members list stays empty.
To fix it, go to Settings > Application in your dashboard, add the domain your site is served from, and set it to Live Mode. Adding a Live Mode domain requires a paid Memberstack plan. On a free plan you can still add and test the domain, but it will remain in Test Mode.
Two useful implications:
- localhost can never be a registered domain, so local development is always in Test Mode. That is the correct and expected behaviour, not a misconfiguration.
- A Test Mode banner appearing on your production site is the visible symptom of this. It means the domain is missing from your dashboard.
For more on how the two modes work, see Managing Test Mode and Live Mode Domains.
8. When you should use npm instead
The script tag is the right answer for static and server rendered sites: plain HTML, Jekyll, Hugo, Eleventy, PHP, Rails views, and similar. Reach for @memberstack/dom from npm when:
- You are building a single page app. The script tag does not re-run on client side navigation, so in a framework with a virtual DOM the attributes will stop applying after the first page.
- You already have a bundler and want type definitions, tree shaking, and imports.
- You are using React, Next.js, Vue, Svelte, or anything with a compile step.
For that route, see Getting Started with the Memberstack DOM Package and the DOM package quick start. Note that the npm route uses your public key, which starts with pk_, rather than the app ID used by the script tag.
9. Common mistakes
- Trying to load @memberstack/dom from a CDN. There is no browser build. Use the script tag from section 3.
- Putting the script at the bottom of the page. It belongs in <head>. Content gating hides protected elements as early as possible, and loading late can cause a flash of gated content.
- Calling methods without the ready check. See section 6.
- Forgetting to register your domain. See section 7.
- Expecting the attributes to survive client side routing. They do not. If you are building an SPA, use the npm package.
This article was previously titled "Vanilla JS Guide".
Comments
10 comments
Hey Memberstack Team,
I'm getting a headache from trying and trying, so I just have to ask now.
You write that you export
as a reusable variable in "lib/memberstack.js", for example.
In modular files you then import the variable with
and try to redeclare this unchangeable variable with
I've been trying to figure this out for 2 hours now to see if I'm seeing something wrong or missing a detail, but my IDE tells me unequivocally that "memberstack" is already declared and this can't work.
I would appreciate a short info if and/or what I am missing here.
Josh Lopez Do you know why this might be happening to Dennis?
Hey Dennis Karg
can you please provide more information about your stack? Are you using react, vue, or sveltekit?
Hey Josh, thank you for your reply and question. My stack consists only of Webflow and vanilla JS with EsBuild. No framework such as Vue or React is involved.
From a purely logical point of view, I don't understand why I should write the following in (let's stick with the example) lib/memberstack.js:
To then redeclare the variable "memberstack" in the "auth.js".
At the moment I've only managed it by putting the memberstack.js in the <head> (which I actually want to avoid with the DOM package) and then writing the following in my JS files to access the object:
I really don't think I've understood something or it simply doesn't work the way I think it should.
Thanks for your efforts Josh!
Are you using the webflow package install script like:
and also using the DOM package? If so the webflow package is a wrapper around the DOM package and is included so you dont need to add both.
I havent seen someone use Webflow with esbuild before. If I am off track maybe a video showing your project would help me understand better.
Hey Josh, thank you for your prompt reply. The script you have mentioned is exactly that.
I had tried to integrate Memberstack into the project exclusively with the DOM package, but that's exactly what somehow didn't want to work (because maybe it's not possible or I made a mistake?) - maybe because of the test mode?
I don't use EsBuild in context with or in Webflow, only for all custom code, which I bundle and thus integrate into my project via <script async src="xyz.com/app.js"></script>.
My basic goal is to use the DOM package instead of the script from "https://static.memberstack.com/scripts/v1/memberstack.js" to run the frontend code from Memberstack exclusively within my bundled JavaScript - just like I use GSAP in these, for example.
I would like to record a video to help you understand it better, but the project is still at a very early stage and I don't want any information to get out yet. If I do, I'll have to send it to you directly.
ah the https://static.memberstack.com/scripts/v1/memberstack.js script is a wrapper around the DOM package so it comes with it. Meaning you dont have to initialize it. You would only need to use:
Try something like this:
Hey Josh Lopez Memberstack Team,
It might be worth adding something about v1.0 vs v2.0.
I am getting a little confused because of this URL you have: https://static.memberstack.com/scripts/v1/memberstack.js.
Does the v1 in it suggest it's for Memberstack v1.0? If you want to build straight on v2.0, should we use a different URL?
I also just realised that you're including the link in angle brackets in the code which is not correct syntax afaik, so perhaps that was meant to mean <change as relevant>.
I can't easily find references on what that maybe should be changed to, so I am gonna have to just play around, but this could be less confusing in this help doc I think. Thanks!
We have a 1.0 to 2.0 conversion doc here.
Please sign in to leave a comment.