MonaKiosk in Action / Getting Started

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:

  1. Go to polar.sh and sign in
  2. Create an organization (or use existing)
  3. Navigate to Settings → Developers → Personal Access Tokens
  4. Create a token with products:read, products:write, benefits:read, benefits:write, customers:read scopes

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 sandbox mode for testing. Polar provides test card numbers like 4242 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 price
  • hasAccess — Whether user can view full content
  • preview — 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:

  1. A preview of the content
  2. A purchase button
  3. 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:

  1. MonaKiosk scans your content collections
  2. Files with price field are synced to Polar as products
  3. Product IDs are cached for runtime lookups

At runtime, the middleware handles access control — we’ll explore this in the Architecture chapter.

Quick Reference

TaskHow
Make content paidAdd price: 999 to frontmatter
Make content freeRemove price field
Change priceUpdate price value, rebuild
Add subscriptionAdd interval: "month" with price

Next, let’s understand Polar.sh and its core concepts.

Full configure reference: MonaKiosk Configuration