Authentication
Add user authentication with platform sessions and OAuth providers
Overview
The Playcademy platform already provides user identity through the SDK.
This authentication integration is optional. Add it based on your needs.
| Feature | Do I need this? |
|---|---|
| Basic user data (ID, username, etc.) | No (Platform SDK provides this) |
| Playcadem data (inventory, credits, etc.) | No (Platform SDK provides this) |
| Custom user fields (avatar, bio, etc.) | Yes (Requires authentication integration) |
| OAuth providers (GitHub, Google) | Yes (Requires authentication integration) |
| Standalone mode (outside Playcademy) | Yes (Requires authentication integration) |
| Account settings & user management | Yes (Requires authentication integration) |
Need help? Ask us on Discord
Getting Started
$ playcademy init # Select "Yes" for Database, then "Yes" for Authentication$ playcademy auth initThis scaffolds a complete authentication setup:
Database Required
Authentication requires a database. If you don't have one yet, you'll be prompted to add it.
Configure Your Secret
After scaffolding, add the required secret to your .env file:
BETTER_AUTH_SECRET=your-secret-hereRequired Secret
BETTER_AUTH_SECRET must be at least 32 characters and generated with high entropy.
The dev server will fail to start without it.
How It Works
The authentication integration provides dual-mode authentication that works both on Playcademy and as a standalone project.
Platform Mode
When launched from Playcademy, users are automatically signed in.
- Platform provides a JWT token that's exchanged for a Better Auth session
- No login UI required; authentication is seamless
Standalone Mode
When accessed directly, users authenticate via your configured providers.
- Email/password, GitHub OAuth, or Google OAuth
- You build the login UI using your own components
Authentication Configuration
The CLI creates server/lib/auth.ts with your Better Auth configuration:
export function getAuth(c: Context) {
const db = getDb(c.env.DB)
return betterAuth({
database: drizzleAdapter(db, { provider: 'sqlite' }),
trustedOrigins: ['http://localhost:5173'],
emailAndPassword: { enabled: true },
plugins: [playcademy()],
advanced: {
defaultCookieAttributes: {
sameSite: 'none',
secure: true,
path: '/',
},
},
})
}This configuration is fully customizable; you can add/remove providers and adjust settings as you see fit.
Using Authentication in Routes
Access the authenticated user via c.get('user'):
import type { User } from '../lib/auth'
export async function POST(c: Context) {
const user = c.get('user') as User | undefined
if (!user) {
return c.json({ error: 'Authentication required' }, 401)
}
// user.id is the Better Auth user ID
// user.playcademyUserId links to platform identity (if launched from Playcademy)
const { level, score } = await c.req.json()
// Save progress for this user...
return c.json({ success: true })
}The CLI creates a sample protected route at server/api/sample/protected.ts demonstrating this pattern.
Frontend Usage
Integrating authentication into your frontend requires:
- Configuring an auth client using the Better Auth SDK
- Adding the Playcademy plugin to the auth client
import { createAuthClient } from 'better-auth/react'
import { playcademy } from '@playcademy/better-auth/client'
export const authClient = createAuthClient({
plugins: [playcademy()],
})Use it in your components:
import { authClient } from './lib/auth'
export function App() {
const { data: session, isPending } = authClient.useSession()
if (isPending) {
return <div>Loading...</div>
}
if (!session) {
// Show login UI
return <div>Please sign in</div>
}
return <div>Welcome, {session.user.name}!</div>
}Adding Authentication Strategies
Email/Password
$ playcademy init # Select Email/password in auth strategies$ playcademy auth add emailOAuth Providers
Add GitHub or Google OAuth:
$ playcademy init # Select GitHub OAuth or Google OAuth$ playcademy auth add github$ playcademy auth add googleCreate OAuth App
Create an OAuth app in the GitHub/Google developer console.
Add Local Credentials
Add credentials to .env for local development:
BETTER_AUTH_SECRET=your-secret-here
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secretPush Secrets
Push secrets to your deployed project:
$ playcademy secrets pushSee Secrets Management for more details.
Configure Callback URL
Set the callback URL in your OAuth app:
https://your-project.playcademy.gg/api/auth/callback/githubDeploy
$ playcademy deployDatabase Schema
The auth schema includes four tables:
/**
* Better Auth Schema
*
* Database tables for Better Auth authentication system
*/
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('emailVerified').notNull(),
image: text('image'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
// Platform linkage: Links this user to a Playcademy platform identity
// When users launch from Playcademy, their platform identity
// is verified and linked to a Better Auth session via this field
playcademyUserId: text('playcademy_user_id').unique(),
})
export const session = sqliteTable('session', {
id: text('id').primaryKey(),
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
token: text('token').notNull().unique(),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: text('userId')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
})
export const account = sqliteTable('account', {
id: text('id').primaryKey(),
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: text('userId')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
idToken: text('idToken'),
accessTokenExpiresAt: integer('accessTokenExpiresAt', { mode: 'timestamp' }),
refreshTokenExpiresAt: integer('refreshTokenExpiresAt', { mode: 'timestamp' }),
scope: text('scope'),
password: text('password'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
})
export const verification = sqliteTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
})Adding Custom User Fields
Extend the user table with project-specific fields:
export const user = sqliteTable('user', {
// ... standard Better Auth fields ...
// Add custom fields
level: integer('level').default(1),
xp: integer('xp').default(0),
avatar: text('avatar'),
})Then push your schema:
$ bun db:pushWhat's Next?
Secrets Management
Learn how to manage secrets for local development and deployment.
Better Auth Documentation
Learn about all Better Auth features and customization options.
Custom Routes
Build API routes that use authentication middleware.
Database Integration
Understand how the database integration works with auth.
