Claude-skill-registry bknd-public-vs-auth
Use when configuring public vs authenticated access in Bknd. Covers anonymous role setup, unauthenticated data access, public/private entity patterns, mixed access modes, and protecting sensitive entities while exposing public ones.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bknd-public-vs-auth" ~/.claude/skills/majiayu000-claude-skill-registry-bknd-public-vs-auth && rm -rf "$T"
skills/data/bknd-public-vs-auth/SKILL.mdPublic vs Authenticated Access
Configure which data and endpoints are publicly accessible vs require authentication.
Prerequisites
- Bknd project with code-first configuration
- Auth enabled (
)auth: { enabled: true } - Guard enabled (
)guard: { enabled: true } - Basic understanding of roles (see bknd-create-role)
When to Use UI Mode
- Viewing current role configurations
- Inspecting permission assignments
UI steps: Admin Panel > Auth > Roles
Note: Access configuration requires code mode.
When to Use Code Mode
- Setting up anonymous/default role for public access
- Configuring entity-specific access rules
- Creating mixed public/private data patterns
- Building closed (auth-required) systems
Core Concept: Default Role
Bknd uses the default role to determine what unauthenticated users can access:
User makes request → Has token? → Yes → Use user's role → No → Use default role (is_default: true) → No default? → ACCESS DENIED
Code Approach
Step 1: Fully Public (Read-Only)
Allow unauthenticated users to read all data:
import { serve } from "bknd/adapter/bun"; import { em, entity, text } from "bknd"; const schema = em({ posts: entity("posts", { title: text().required() }), }); serve({ connection: { url: "file:data.db" }, config: { data: schema.toJSON(), auth: { enabled: true, guard: { enabled: true }, roles: { // Public role - anyone can read anonymous: { is_default: true, implicit_allow: false, permissions: ["data.entity.read"], }, // Authenticated users can create/update user: { implicit_allow: false, permissions: [ "data.entity.read", "data.entity.create", "data.entity.update", ], }, }, }, }, });
Result:
- Works without authGET /api/data/posts
- Requires authPOST /api/data/posts
- Requires authPATCH /api/data/posts/1
Step 2: Fully Private (Auth Required)
Require authentication for all access:
{ auth: { enabled: true, guard: { enabled: true }, allow_register: true, default_role_register: "user", roles: { admin: { implicit_allow: true }, user: { implicit_allow: false, permissions: [ "data.entity.read", "data.entity.create", "data.entity.update", ], }, // NO default role - unauthenticated users get nothing }, }, }
Result: All
/api/data/* endpoints return 403 without authentication.
Step 3: Entity-Specific Public Access
Make some entities public, others private:
{ auth: { enabled: true, guard: { enabled: true }, roles: { anonymous: { is_default: true, implicit_allow: false, permissions: [ // Only posts are public { permission: "data.entity.read", effect: "allow", policies: [{ condition: { entity: "posts" }, effect: "allow", }], }, ], }, user: { implicit_allow: false, permissions: [ "data.entity.read", // Read all entities "data.entity.create", "data.entity.update", ], }, }, }, }
Result:
- PublicGET /api/data/posts
- Requires authGET /api/data/users
- Requires authGET /api/data/comments
Step 4: Multiple Public Entities
Expose several entities publicly:
{ roles: { anonymous: { is_default: true, implicit_allow: false, permissions: [ { permission: "data.entity.read", effect: "allow", policies: [{ condition: { entity: { $in: ["posts", "categories", "tags"] } }, effect: "allow", }], }, ], }, }, }
Step 5: Public Records with Filter
Make only published/public records accessible:
{ roles: { anonymous: { is_default: true, implicit_allow: false, permissions: [ { permission: "data.entity.read", effect: "allow", policies: [ // Posts: only published { condition: { entity: "posts" }, effect: "filter", filter: { status: "published" }, }, // Products: only visible { condition: { entity: "products" }, effect: "filter", filter: { visible: true }, }, ], }, ], }, }, }
Result: Anonymous users only see filtered records; authenticated users see all.
Step 6: Mixed Public/Owner Access
Public can read published; owners can read their own drafts:
{ roles: { anonymous: { is_default: true, implicit_allow: false, permissions: [ { permission: "data.entity.read", effect: "allow", policies: [{ condition: { entity: "posts" }, effect: "filter", filter: { status: "published" }, }], }, ], }, user: { implicit_allow: false, permissions: [ // Read: published OR own posts { permission: "data.entity.read", effect: "allow", policies: [{ condition: { entity: "posts" }, effect: "filter", filter: { $or: [ { status: "published" }, { author_id: "@user.id" }, ], }, }], }, // Create allowed "data.entity.create", // Update own only { permission: "data.entity.update", effect: "allow", policies: [{ effect: "filter", filter: { author_id: "@user.id" }, }], }, ], }, }, }
Step 7: Invite-Only System
No public access, no self-registration:
{ auth: { enabled: true, guard: { enabled: true }, allow_register: false, // Disable self-registration roles: { admin: { implicit_allow: true }, member: { implicit_allow: false, permissions: [ "data.entity.read", "data.entity.create", "data.entity.update", ], }, // No default role }, }, options: { seed: async (ctx) => { // Admin creates users manually await ctx.app.module.auth.createUser({ email: "admin@company.com", password: "admin-password", role: "admin", }); }, }, }
Step 8: API with Public Read, Auth Write
Common REST API pattern:
{ roles: { anonymous: { is_default: true, implicit_allow: false, permissions: ["data.entity.read"], // Read anything }, api_user: { implicit_allow: false, permissions: [ "data.entity.read", "data.entity.create", "data.entity.update", "data.entity.delete", ], }, }, }
Complete Configuration Examples
Blog Platform
import { serve } from "bknd/adapter/bun"; import { em, entity, text, boolean, relation } from "bknd"; const schema = em( { posts: entity("posts", { title: text().required(), content: text(), published: boolean().default(false), }), comments: entity("comments", { body: text().required(), approved: boolean().default(false), }), users: entity("users", {}), }, ({ posts, comments, users }) => [ relation(posts, "author").manyToOne(users), relation(comments, "post").manyToOne(posts), relation(comments, "user").manyToOne(users), ] ); serve({ connection: { url: "file:data.db" }, config: { data: schema.toJSON(), auth: { enabled: true, guard: { enabled: true }, allow_register: true, default_role_register: "commenter", roles: { // Public: read published posts + approved comments anonymous: { is_default: true, implicit_allow: false, permissions: [ { permission: "data.entity.read", effect: "allow", policies: [ { condition: { entity: "posts" }, effect: "filter", filter: { published: true }, }, { condition: { entity: "comments" }, effect: "filter", filter: { approved: true }, }, ], }, ], }, // Registered users: read all, create comments commenter: { implicit_allow: false, permissions: [ "data.entity.read", { permission: "data.entity.create", effect: "allow", policies: [{ condition: { entity: "comments" }, effect: "allow", }], }, ], }, // Authors: full post access, manage own comments author: { implicit_allow: false, permissions: [ "data.entity.read", { permission: "data.entity.create", effect: "allow", policies: [{ condition: { entity: { $in: ["posts", "comments"] } }, effect: "allow", }], }, { permission: "data.entity.update", effect: "allow", policies: [{ condition: { entity: "posts" }, effect: "filter", filter: { author_id: "@user.id" }, }], }, ], }, // Admin: everything admin: { implicit_allow: true }, }, }, }, });
SaaS Application
{ auth: { enabled: true, guard: { enabled: true }, allow_register: true, default_role_register: "free_user", roles: { // Landing page data only anonymous: { is_default: true, implicit_allow: false, permissions: [ { permission: "data.entity.read", effect: "allow", policies: [{ condition: { entity: { $in: ["plans", "features"] } }, effect: "allow", }], }, ], }, // Free tier: limited access free_user: { implicit_allow: false, permissions: [ "data.entity.read", { permission: "data.entity.create", effect: "allow", policies: [{ condition: { entity: "projects" }, effect: "allow", }], }, ], }, // Paid tier: full access to own data pro_user: { implicit_allow: false, permissions: [ "data.entity.read", "data.entity.create", { permission: "data.entity.update", effect: "allow", policies: [{ effect: "filter", filter: { owner_id: "@user.id" }, }], }, { permission: "data.entity.delete", effect: "allow", policies: [{ effect: "filter", filter: { owner_id: "@user.id" }, }], }, ], }, admin: { implicit_allow: true }, }, }, }
Testing Access Levels
Test Public Access
# Should succeed (anonymous read) curl http://localhost:7654/api/data/posts # Should fail (anonymous create) curl -X POST http://localhost:7654/api/data/posts \ -H "Content-Type: application/json" \ -d '{"title": "Test"}' # Returns 403
Test Authenticated Access
# Login TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \ -H "Content-Type: application/json" \ -d '{"email": "user@test.com", "password": "pass123"}' | jq -r '.token') # Should succeed (authenticated create) curl -X POST http://localhost:7654/api/data/posts \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"title": "Test"}'
Test Entity-Specific Access
# Public entity - should succeed curl http://localhost:7654/api/data/posts # Private entity - should fail curl http://localhost:7654/api/data/users # Returns 403
Test Filtered Access
# Anonymous: only sees published curl http://localhost:7654/api/data/posts # Returns: [{ status: "published" }, ...] # Authenticated: sees all including drafts curl http://localhost:7654/api/data/posts \ -H "Authorization: Bearer $TOKEN" # Returns: [{ status: "draft" }, { status: "published" }, ...]
Frontend Integration
React: Check Auth State
import { useApp, useAuth } from "bknd/react"; function DataDisplay() { const { api } = useApp(); const { user } = useAuth(); const [posts, setPosts] = useState([]); useEffect(() => { // Works for both anonymous and authenticated api.data.readMany("posts").then((res) => { if (res.ok) setPosts(res.data); }); }, []); return ( <div> {posts.map((post) => ( <article key={post.id}> <h2>{post.title}</h2> {/* Show edit only for authenticated users */} {user && <button>Edit</button>} </article> ))} {/* Show create only for authenticated */} {user ? ( <button>New Post</button> ) : ( <a href="/login">Login to create posts</a> )} </div> ); }
Conditional Fetch
function useProtectedData(entity: string) { const { api } = useApp(); const { user, isLoading } = useAuth(); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { if (isLoading) return; api.data.readMany(entity).then((res) => { if (res.ok) { setData(res.data); } else { setError(res.error); } }); }, [entity, user, isLoading]); return { data, error, isAuthenticated: !!user }; } // Usage function ProtectedPage() { const { data, error, isAuthenticated } = useProtectedData("projects"); if (error?.status === 403 && !isAuthenticated) { return <LoginPrompt />; } return <DataList items={data} />; }
Common Pitfalls
No Default Role = No Public Access
Problem:
Permission not granted for unauthenticated requests
Fix: Add a default role:
{ roles: { anonymous: { is_default: true, // Required for public access! permissions: ["data.entity.read"], }, }, }
Guard Disabled
Problem: Everyone can access everything
Fix: Enable the guard:
{ auth: { enabled: true, guard: { enabled: true }, // Required! }, }
Filter Not Applied
Problem: Anonymous users see all records, not just filtered
Fix: Use
effect: "filter" not effect: "allow":
// WRONG - allows all { condition: { entity: "posts" }, effect: "allow", filter: { published: true }, // Ignored! } // CORRECT - applies filter { condition: { entity: "posts" }, effect: "filter", filter: { published: true }, }
Sensitive Entity Exposed
Problem: Users entity publicly readable
Fix: Use entity conditions:
{ permissions: [ { permission: "data.entity.read", effect: "allow", policies: [{ // Only allow specific entities condition: { entity: { $in: ["posts", "comments"] } }, effect: "allow", }], }, ], }
Auth Header Not Sent
Problem: User authenticated but still gets public data
Fix: Include credentials in fetch:
// Browser with cookies fetch("/api/data/posts", { credentials: "include" }); // Token-based fetch("/api/data/posts", { headers: { Authorization: `Bearer ${token}` }, });
Access Matrix Reference
| Scenario | Anonymous Role | User Role | Result |
|---|---|---|---|
| Public Read | | All CRUD | Anon: read; User: CRUD |
| Private Only | None/No default | All CRUD | Anon: 403; User: CRUD |
| Entity-Specific | Read posts only | Read all | Anon: posts; User: all |
| Filtered | Filter published | Read all | Anon: published; User: all |
DOs and DON'Ts
DO:
- Set
on exactly one role for public accessis_default: true - Use entity conditions to limit which entities are public
- Use filter policies to expose only appropriate records
- Test access as both anonymous and authenticated users
- Keep sensitive entities (users, settings) protected
DON'T:
- Forget to enable guard (
)guard: { enabled: true } - Use
on anonymous/default roleimplicit_allow: true - Expose user data publicly without filters
- Assume auth header is always sent (check frontend code)
- Mix up
andeffect: "allow"effect: "filter"
Related Skills
- bknd-create-role - Define roles for authorization
- bknd-assign-permissions - Configure detailed permissions
- bknd-row-level-security - Data-level access control
- bknd-protect-endpoint - Secure custom endpoints
- bknd-setup-auth - Initialize authentication system