You don't need to build auth.
You never did.
Astapa is a complete backend for user auth, subscription plans, and Indonesian payments — delivered as a hosted API. Redirect users to our login page, get back a signed JWT, read the plan from it. That's the whole integration.
What you actually get
Not a list of features — a list of things you no longer have to build.
Auth you didn't write
Email/password, Google, GitHub OAuth. Signup, email verification, password resets — all hosted. You just redirect.
Plans that live in the JWT
Create tiers in the dashboard. Assign them to users. The plan shows up in the token — your app just reads it.
Indonesian payments, done
QRIS, GoPay, ShopeePay, bank transfers, credit cards. One API call creates a Midtrans checkout. We handle the rest.
Auth for AI agents too
MCP servers and AI agents need auth. client_credentials flow, scoped JWTs, same verification logic.
Three steps, then you're done
Seriously. Most integrations take an afternoon.
Create a project, grab your credentials
Go to your dashboard, create a project, add your callback URL as a redirect URI. You get a client_id (public, safe for the browser) and a client_secret (private, server only).
ASTAPA_CLIENT_ID=your_client_id
ASTAPA_CLIENT_SECRET=your_client_secret # never expose this
NEXT_PUBLIC_ASTAPA_CLIENT_ID=your_client_idRedirect to our login page
When a user needs to log in, send them here. We handle the UI, the OAuth dance, email verification — everything. They come back to your app with a code.
const params = new URLSearchParams({
client_id: process.env.NEXT_PUBLIC_ASTAPA_CLIENT_ID!,
redirect_uri: "https://yourapp.com/callback",
state: crypto.randomUUID(), // store in cookie for CSRF check
});
// Send the user here
window.location.href = `https://astapa.com/auth/login?${params}`;Exchange the code, store the tokens
Your callback receives ?code=.... Exchange it server-side for an access token (JWT) and a refresh token. Store both in httpOnly cookies.
const res = await fetch("https://astapa.com/api/platform/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code: searchParams.get("code"),
client_id: process.env.ASTAPA_CLIENT_ID,
client_secret: process.env.ASTAPA_CLIENT_SECRET,
redirect_uri: "https://yourapp.com/callback",
}),
});
const { access_token, refresh_token } = await res.json();
// access_token is a signed JWT — verify it with our JWKS endpoint
// store both in httpOnly cookiesWhat's inside the JWT
Decode any access token and you'll find everything you need. No extra API calls to identify the user or check their plan.
{
"sub": "42",
"email": "user@example.com",
"email_verified": true,
"project_id": "proj_abc123",
"custom_claims": {
"plan": "pro",
"role": "admin",
"feature_flags": ["beta_dashboard", "export_csv"]
},
"iss": "https://astapa.com",
"aud": "your_client_id",
"exp": 1712345678,
"iat": 1712342078
}custom_claims.plan from the JWT. Gate features in your app. When a user upgrades, set their new plan via the Claims API, refresh the token, done. No database queries, no extra API calls.Tokens expire. Here's what to do about it.
Access tokens last 1 hour. Refresh tokens last much longer. Your middleware should silently refresh before the access token expires — the user never notices.
POST /api/platform/token with grant_type=refresh_token. You get a fresh JWT with the latest claims baked in.POST /api/platform/revoke to kill all refresh tokens, then clear your cookies. The user can't refresh anymore — they're out.Use sandbox while you build
Every project has a sandbox environment with its own sandbox_client_id and sandbox_client_secret. Sandbox users are completely isolated from production. Payments route to Midtrans sandbox automatically — no real money moves.
Base URL
https://astapa.com/api/platform/...Where to go next
Pick the thing you're building right now.