--- name: bknd-public-vs-auth description: 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. --- # Public 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: ```typescript 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:** - `GET /api/data/posts` - Works without auth - `POST /api/data/posts` - Requires auth - `PATCH /api/data/posts/1` - Requires auth ### Step 2: Fully Private (Auth Required) Require authentication for all access: ```typescript { 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: ```typescript { 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:** - `GET /api/data/posts` - Public - `GET /api/data/users` - Requires auth - `GET /api/data/comments` - Requires auth ### Step 4: Multiple Public Entities Expose several entities publicly: ```typescript { 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: ```typescript { 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: ```typescript { 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: ```typescript { 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: ```typescript { 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 ```typescript 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 ```typescript { 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 ```bash # 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 ```bash # 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 ```bash # 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 ```bash # 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 ```typescript 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 (