Context
Following up from the article, Registering Your Site on Paperwall, this is a comprehensive integration guide to the SDK provided by Paperwall. The goal in creating this SDK is to remain as aesthetically agnostic as possible, but provide a straightforward means of integration. This article walks through the technical details to integrate your pre-existing paywall logic with Paperwall’s. The end result might look something like this:

You can find additional details about registering on paperwall.io and you can follow these instructions to set up your site and articles.
Technical Walkthrough
The process can be is broken down to 4 main steps:
Install the library using your package manager of choice
Configure the paperwall app with settings highlighted in the onboarding wizard on paperwall.io
Add event listeners to the stores/context for specific events emitted by paperwall-lib
Trigger actions based on the listeners using built in life-cycle methods
useEffect,$effect, etc)
That’s it. From there, everything else can be configured through the site manager at paperwall.io.
1. Install into your code
Follow the instructions on the homepage at paperwall-lib to install it into your frontend application. It is not currently listed as an npm package, so it will have to be installed directly from github.
"paperwall": "github:paperwall-io/paperwall-lib"This will be available in the future as an NPM package, but it is actively being worked on.
2. Add the app configuration
Add the following configuration to the main/index/server.ts file - this will act as an entry-point to your application and exposes a singleton you can reference throughout the rest of your application.
export default initPaperwall({
// available through the site registration wizard
siteToken: "<Your site token>",
// optional, if you only want to initiate per post
articleFinder: { //
selector: "<blog-post-id>",
postUrls: [/posts\/\w+\/?/],
},
})3. Add event listeners
paperwall-lib uses an event based model whenever changes happen - whether it be the overall state or the entities being updated. There is some back and forth and complexity in how the app initializes, so this helps to minimize the effort to integrate.
Svelte is the framework of choice for PaperWall, so it will be used as the primary reference for now.
<script lang='ts'>
// in App.svelte
// assuming you have a Svelte store called `$wallStore`
onMount(() => {
// create listeners in the store
const unsubEntities = pw.entities.sub((state: WallStore) => {
$wallStore.entities = state;
});
const unsubWallState = pw.wallState.sub((state: WallState) => {
console.log("WallState", state);
$wallStore.wallState = state;
});
const navUnsub = pw.resetOnNav();
return () => {
unsubEntities();
unsubWallState();
navUnsub();
};
});
</script>This is listening for any updates to either the entities provided by Paperwall or any state changes, which are triggered by certain conditions after interacting with the API. There is another listener, resetOnNav() to reset the app whenever any navigation occurs, but that is optional. It restores the state to LOADING to re-trigger the workflow using cached information wherever possible.
wallState contains the latest state of the app and includes:
// Available WallStates
"LOADING"
| "INIT"
| "QUICK_AUTH"
| "INIT_SESSION"
| "NO_WALL"
| "SHOW_WALL"
| "SHOW_ARTICLE";LOADING- the initial app. It runs through a bunch of checks for query parameters to see if any special commands need to be run, includingquick-auth,paperwall-token, andpaperwall-reset(more details can be found in the next section, initApp).INIT- Triggers the initiation of an individual article and member’s session. Loads the following information:article- the information provided about the articlereport- analytics about the article’s activity in paperwallflags- any flags that have been set for a specific article, including whether it is in preview modearticleSession- user_id, has_purchased, pricing, is_site_memberbalance- the current paperwall member’s balance
NO_WALL- if an article is not configured or eligible for a wall to show up, this will be the state returned after all the interactions are completeSHOW_WALL- an article has been configured but the member has not yet purchased, or is not eligible to see the article yetSHOW_ARTICLE- the article has been redeemed by a member
There are a couple internal flags that are exposed, but are largely used internally by paperwall-lib to determine the next step
QUICK_AUTH- The initial exchange of site tokens to authenticate a member. Once this is complete is when the rest of the app will re-start the initiation after removing it from the search-query. It then transitions to theLOADINGstate.INIT_SESSION- this is another flag simply indicating that an article and/or session calls are occurring.
4. Instantiate the app
There are 2 different initiation functions to call to trigger Paperwall, initApp and initArticle.
initApp
<script lang='ts'>
$effect(() => {
if ($wallStore.wallState === "LOADING") {
pw.initApp();
}
});
</script>In Svelte, the line $wallStore.wallState === "LOADING" is sufficient for the event listener.
If you want to include a reference to the DOM element to ensure it exists before moving onto the next step, you can expand that code as follows:
<script lang='ts'>
let isPost = $state(false)
$effect(() => {
if ($wallStore.wallState === "LOADING") {
tick().then(() => {
pw.initApp();
isPost = pw.detectIsPost();
});
}
});
</script>
{#if isPost}
<BlogPost ... />
{/if}A comparable implementation in React would use the following to subscribe to any state updates:
import type { WallState } from 'paperwall'
import pw from 'src/main';
const [wallStore, setWallStore] = useContext<{state: WallState}>({
state: 'LOADING',
});
useEffect(() => {
pw.wallState.sub((newState: WallState) => {
console.log("WallState", newState);
setWallStore({state: newState})
});
}, [])
useEffect(() => {
if(wallStore.state === 'LOADING'){
pw.initApp()
}
}, [wallStore.state])initApp is only triggered when wallState===LOADING. It runs through the browser’s search query for the following:
Register the site -
?paperwall-token=<site_token>Reset the app -
?paperwall-reset=true- clears browser storageAuthorize session -
?quick-auth=<auth-token>takes a short-lived one-use token from paperwall and exchanges it for a longer use token that identifies the Paperwall member on the site. This is the primary bridge between paperwall.io and the hosting site. The resulting token,siteSessiondefaults to a 30 day expiry and only allows read-only access to the information provided paperwall.io.
The quick-auth param is essential for getting the rest of the app to communicate with paperwall.io, which is why this is critical to trigger first. It can be done from any layout page, which will cascade to the individual blog-posts or from templated blog-posts page. It won’t run every time, just when a member needs to connect their session with paperwall.io (i.e., the query parameter is present).
From there, it will either transition the app to INIT or, QUICK_AUTH → LOADING, which restarts the application’s transitions.
detectIsPost is an optional function in which Paperwall identifies the DOM element where the post is mounted, based on the initial config options:
articleFinder: { //
selector: "<blog-post-id>",
postUrls: [/posts\/\w+\/?/],
},This trigger prevents unnecessary initializations looking for a post that doesn’t exist and provides a couple other helpers, like styling options and establishing read-time for an article (often a valuable metric in ensuring an article is worth paying for).
initArticle
<script lang='ts'>
import pw from 'src/main';
$effect(() => {
if ($wallStore.wallState === "INIT") {
console.log("triggering initArticle");
pw.initArticle();
}
});
</script>initArticle is only triggered when the article is in an INIT state and runs through the necessary calls to load the app. You can see a high level flow diagram here:

As you can see, it does a lot of internal interactions, but the only resulting states to worry about are NO_WALL, SHOW_WALL and SHOW_ARTICLE. At that point, all the necessary information will be available in the entities to build out the wall as you see fit. wallState can be referenced anywhere else in the app to augment any other logic indicating whether an article can be read or not.
This is the last helper, and is optional. It listens for all the different browser navigations (popState, hashState, pushState, replaceState) and when any of those are triggered performs the following resets to help re-initialize the app:
nullifies the article DOM element - this prevents false positives when navigating away from blog-posts
sets
wallState=LOADING- ensures the app is in the initial state to re-initialize the flow
resetOnNav doesn’t need to be called in this way and can be managed by calling resetArticleEl and wallState.set(‘LOADING’) from wherever makes the most sense in the app.
It may seem involved, but conditions are put in place to ensure it only initializes when it needs to; the logic is as minimal, but helpful, as possible; and it can seamlessly integrate into existing code.
Summary
Integrating with paperwall.io is a streamlined experience, involving 4 main steps, with the meatiest being step 4 - integrating the app’s logic and listening for the appropriate state changes. Luckily, the major frameworks, Svelte, React, NextJS, follow a similar pattern of updating on state changes and are able to subscribe to the Paperwall events.
The next steps involve verifying your site to proceed and then configuring any articles where you’d like the paywall to show up.
Any questions or feedback can be sent to paperwall.io/contact.

