Installation & Quick Start
This chapter walks you through installing MonaKiosk and creating your first paywalled content.
Prerequisites
Before starting, ensure you have:
- Node.js 18+ installed
- An existing Astro project (or create one with
pnpm create astro@latest) - A Polar.sh account (free to create)
Step 1: Install Dependencies
pnpm add mona-kiosk
MonaKiosk includes the Polar SDK internally, so you don’t need to install it separately.
You’ll also need a server adapter since MonaKiosk requires SSR:
pnpm astro add node # or vercel, netlify, etc.
Step 2: Configure Polar
Create a Polar organization and generate an access token:
- Go to polar.sh and sign in
- Create an organization (or use existing)
- Navigate to Settings → Developers → Personal Access Tokens
- Create a token with
products:read,products:write,benefits:read,benefits:write,customers:readscopes
Add the credentials to your .env:
POLAR_ACCESS_TOKEN=polar_oat_xxxxxxxxxxxxx
POLAR_ORG_SLUG=your-org-slug
POLAR_ORG_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
POLAR_SERVER=sandbox # Use 'production' for live payments
# Generate with: openssl rand -base64 32
# You may also use better-auth generate secret button: https://www.better-auth.com/docs/installation#set-environment-variables
ACCESS_COOKIE_SECRET=your-random-secret-string
Tip: Start with
sandboxmode for testing. Polar provides test card numbers like4242 4242 4242 4242.
Step 3: Add the Integration
Update astro.config.mjs:
import node from "@astrojs/node";
import { defineConfig } from "astro/config";
import { monaKiosk } from "mona-kiosk";
import { loadEnv } from "vite";
const {
POLAR_ACCESS_TOKEN,
POLAR_ORG_SLUG,
POLAR_ORG_ID,
POLAR_SERVER,
ACCESS_COOKIE_SECRET,
} = loadEnv(process.env.NODE_ENV ?? "dev", process.cwd(), "");
// https://astro.build/config
export default defineConfig({
output: "server", // Required for MonaKiosk
adapter: node({ mode: "standalone" }),
integrations: [
monaKiosk({
polar: {
accessToken: POLAR_ACCESS_TOKEN,
organizationSlug: POLAR_ORG_SLUG,
organizationId: POLAR_ORG_ID,
server: POLAR_SERVER || "sandbox",
},
siteUrl: "http://localhost:4321", // Your site URL
accessCookieSecret: ACCESS_COOKIE_SECRET,
collections: [{ include: "src/content/blog/**/*.md" }],
}),
],
});
If you were using pnpm, make sure installed vite as a dev dependency:
pnpm add -D vite
See: Astro Environment Variables
Step 4: Update Content Schema
Import PayableMetadata and merge it into your collection schema.
In src/content.config.ts:
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
import { PayableMetadata } from "mona-kiosk";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.md" }),
schema: z
.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
})
.merge(PayableMetadata), // Add paywall fields
});
export const collections = { blog };
PayableMetadata adds these optional fields:
price— Price in cents (e.g.,999= $9.99)currency— Currency code (Currently only “usd” is supported. read Polar.sh Doc)interval— For subscriptions:"month","year","week", or"day"downloads— Array of downloadable files
Step 5: Create Paywalled Content
Create a blog post with a price:
---
title: "Premium Article"
description: "This is premium content worth paying for."
pubDate: 2025-01-30
price: 499 # $4.99
---
This content is only visible to customers who have purchased access.
## The Secret Sauce
Here's the valuable information...
Step 6: Update Page Template
Modify your blog post page to handle paywall state.
In src/pages/blog/[...slug].astro:
---
import { getEntry, render } from "astro:content";
import Layout from "../../layouts/Layout.astro";
const { slug } = Astro.params;
const post = await getEntry("blog", slug!);
if (!post) return Astro.redirect("/404");
const { Content } = await render(post);
const paywall = Astro.locals.paywall;
---
<Layout title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
{paywall?.isPayable && !paywall.hasAccess ? (
<div set:html={paywall.preview} />
) : (
<Content />
)}
</article>
</Layout>
The middleware automatically sets Astro.locals.paywall with:
isPayable— Whether content has a pricehasAccess— Whether user can view full contentpreview— HTML with content preview + purchase UI
Step 7: Build and Test
pnpm build # Syncs products to Polar
pnpm preview
Visit your blog post. You should see:
- A preview of the content
- A purchase button
- Price displayed
Click “Purchase” to test the checkout flow (use test card 4242 4242 4242 4242).
What Just Happened?
sequenceDiagram
participant Dev as Developer
participant Astro as Astro Build
participant MK as MonaKiosk
participant Polar as Polar.sh
Dev->>Astro: pnpm build
Astro->>MK: Integration hook
MK->>MK: Scan content for price fields
MK->>Polar: Create/update products
Polar-->>MK: Product IDs
MK->>MK: Cache product mappings
Astro->>Dev: Build complete
During build:
- MonaKiosk scans your content collections
- Files with
pricefield are synced to Polar as products - Product IDs are cached for runtime lookups
At runtime, the middleware handles access control — we’ll explore this in the Architecture chapter.
Quick Reference
| Task | How |
|---|---|
| Make content paid | Add price: 999 to frontmatter |
| Make content free | Remove price field |
| Change price | Update price value, rebuild |
| Add subscription | Add interval: "month" with price |
Next, let’s understand Polar.sh and its core concepts.
Full configure reference: MonaKiosk Configuration