Authentication Setup
Vinoflare templates use Better Auth for authentication, with Discord OAuth pre-configured. This guide covers setup, customization, and best practices.
Initial Setup
1. Create Discord Application
- Go to Discord Developer Portal
- Click "New Application"
- Name your application
- Go to OAuth2 → General
- Add redirect URLs:
- Development:
http://localhost:8787/api/auth/callback/discord
- Production:
https://yourdomain.com/api/auth/callback/discord
2. Get Credentials
In your Discord app settings:
- Copy Client ID
- Copy Client Secret (under "Reset Secret")
# .dev.vars
BETTER_AUTH_SECRET=your-secret-key-min-32-chars
DISCORD_CLIENT_ID=your-discord-client-id
DISCORD_CLIENT_SECRET=your-discord-client-secret
Generate a secure secret:
Better Auth Configuration
Server Setup
// src/server/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { discord } from "better-auth/social-providers";
import { db } from "@/server/db";
export function createAuth(env: Env) {
return betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
}),
secret: env.BETTER_AUTH_SECRET,
socialProviders: {
discord: {
clientId: env.DISCORD_CLIENT_ID,
clientSecret: env.DISCORD_CLIENT_SECRET,
},
},
trustedOrigins: [
"http://localhost:5173",
"http://localhost:8787",
env.FRONTEND_URL, // Production URL
],
});
}
Database Schema
Better Auth automatically creates these tables:
users
- User accounts
sessions
- Active sessions
accounts
- OAuth provider accounts
verification_tokens
- Email verification
You can extend the user table:
// src/server/db/schema.ts
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name"),
image: text("image"),
// Add custom fields
role: text("role").default("user"),
bio: text("bio"),
createdAt: integer("created_at", { mode: "timestamp" }),
});
Frontend Integration
Auth Client
// src/client/lib/auth.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.DEV
? "http://localhost:8787"
: window.location.origin,
});
export const {
signIn,
signOut,
useSession,
} = authClient;
Login Component
// src/client/components/login-button.tsx
import { signIn } from "@/client/lib/auth";
export function LoginButton() {
const handleLogin = async () => {
await signIn.social({
provider: "discord",
callbackURL: "/dashboard",
});
};
return (
<button
onClick={handleLogin}
className="flex items-center gap-2 px-4 py-2 bg-[#5865F2] text-white rounded-lg hover:bg-[#4752C4] transition-colors"
>
<DiscordIcon className="w-5 h-5" />
Sign in with Discord
</button>
);
}
Protected Routes
// src/client/components/protected-route.tsx
import { useSession } from "@/client/lib/auth";
import { Navigate } from "@tanstack/react-router";
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { data: session, isPending } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (!session) {
return <Navigate to="/login" />;
}
return <>{children}</>;
}
Use in routes:
// src/client/routes/dashboard.tsx
export const Route = createFileRoute("/dashboard")({
component: () => (
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
),
});
Backend Protection
Auth Middleware
// src/server/middleware/auth-guard.ts
import type { Context, Next } from "hono";
import { createAuth } from "@/server/lib/auth";
export function authGuard() {
return async (c: Context, next: Next) => {
const auth = createAuth(c.env);
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
c.set("user", session.user);
c.set("session", session);
await next();
};
}
Protect API Routes
// src/server/modules/posts/posts.routes.ts
import { authGuard } from "@/server/middleware/auth-guard";
export const postsRouter = createRouter()
// Public routes
.get("/", handlers.getPosts)
.get("/:id", handlers.getPost)
// Protected routes
.use("*", authGuard())
.post("/", handlers.createPost)
.put("/:id", handlers.updatePost)
.delete("/:id", handlers.deletePost);
Access User in Handlers
// src/server/modules/posts/posts.handlers.ts
export async function createPost(c: Context) {
const user = c.get("user");
const data = c.req.valid("json");
const post = await db.insert(posts).values({
...data,
authorId: user.id,
authorName: user.name,
}).returning();
return c.json(post[0], 201);
}
Session Management
Get Current User
// Frontend
const { data: session } = useSession();
const user = session?.user;
// Backend
const user = c.get("user");
Sign Out
// Frontend
import { signOut } from "@/client/lib/auth";
function LogoutButton() {
const handleLogout = async () => {
await signOut();
window.location.href = "/";
};
return <button onClick={handleLogout}>Sign Out</button>;
}
Session Refresh
Better Auth automatically refreshes sessions. Configure the behavior:
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Update after 1 day
},
});
Adding More Providers
GitHub OAuth
import { github } from "better-auth/social-providers";
export const auth = betterAuth({
socialProviders: {
discord: {
clientId: env.DISCORD_CLIENT_ID,
clientSecret: env.DISCORD_CLIENT_SECRET,
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
},
},
});
Google OAuth
import { google } from "better-auth/social-providers";
export const auth = betterAuth({
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
});
Email/Password Auth
Enable Email Auth
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
// Email provider for sending verification emails
email: {
from: "noreply@yourdomain.com",
sendEmail: async ({ to, subject, html }) => {
// Use your email service (Resend, SendGrid, etc.)
await resend.emails.send({
from: "noreply@yourdomain.com",
to,
subject,
html,
});
},
},
});
import { authClient } from "@/client/lib/auth";
function SignUpForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const { error } = await authClient.signUp.email({
email,
password,
});
if (error) {
toast.error(error.message);
} else {
toast.success("Check your email for verification");
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}
Role-Based Access Control
Define Roles
// src/server/lib/permissions.ts
export const ROLES = {
USER: "user",
ADMIN: "admin",
MODERATOR: "moderator",
} as const;
export type Role = typeof ROLES[keyof typeof ROLES];
export const PERMISSIONS = {
[ROLES.USER]: ["read:own", "write:own"],
[ROLES.MODERATOR]: ["read:all", "write:own", "moderate:content"],
[ROLES.ADMIN]: ["read:all", "write:all", "admin:all"],
};
Role Middleware
export function requireRole(roles: Role[]) {
return async (c: Context, next: Next) => {
const user = c.get("user");
if (!user || !roles.includes(user.role)) {
return c.json({ error: "Forbidden" }, 403);
}
await next();
};
}
// Usage
adminRouter
.use("*", authGuard())
.use("*", requireRole([ROLES.ADMIN]))
.get("/users", handlers.getAllUsers);
Frontend Role Check
function useRole() {
const { data: session } = useSession();
return {
isAdmin: session?.user?.role === ROLES.ADMIN,
isModerator: [ROLES.ADMIN, ROLES.MODERATOR].includes(session?.user?.role),
hasRole: (role: Role) => session?.user?.role === role,
};
}
// Usage
function AdminPanel() {
const { isAdmin } = useRole();
if (!isAdmin) {
return <div>Access denied</div>;
}
return <div>Admin content</div>;
}
Security Best Practices
1. Secure Cookies
export const auth = betterAuth({
cookie: {
secure: true, // HTTPS only in production
httpOnly: true, // Not accessible via JavaScript
sameSite: "lax", // CSRF protection
},
});
2. CORS Configuration
app.use("*", cors({
origin: (origin) => {
const allowed = [
"http://localhost:5173",
"https://yourdomain.com",
];
return allowed.includes(origin) ? origin : null;
},
credentials: true,
}));
3. Rate Limiting
import { rateLimiter } from "hono-rate-limiter";
app.use("/api/auth/*", rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 5, // 5 attempts
keyGenerator: (c) => c.req.header("CF-Connecting-IP") || "unknown",
}));
const signUpSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
});
Troubleshooting
Common Issues
"Unauthorized" errors
- Check
trustedOrigins
includes your URL
- Verify cookies are being sent
- Check CORS configuration
Session not persisting
- Ensure
credentials: "include"
in fetch
- Check cookie settings
- Verify domain matches
OAuth redirect fails
- Update redirect URLs in provider settings
- Check environment variables
- Verify callback URL format
Debug Mode
export const auth = betterAuth({
logger: {
level: "debug",
},
});
Next Steps