Community
Complete a safe public feed and hosted-customer comment with independent readback.
Community
Prepare
Enable community and its customers dependency. Prepare a published-visible post, not hidden/rejected by moderation, unlocked with allowsComments enabled. Assign moderation/support ownership. Public reads use a Publishable Key; participation uses a same-workspace customer JWT. A Secret Key does not impersonate a customer. Copy all Customers files for hosted client/origin/redirect → state/PKCE callback → Secure HttpOnly session → me() → hosted logout. Existing direct apps use only that guide’s explicit legacy compatibility section.
These examples use the published SDK 0.65.1, Next.js 16.3.8 App Router, React 19.2.4, and Node 24.15.0. Install the SDK in your own app with npm install @01.software/sdk@0.65.1. The examples use force-dynamic; SDK calls run on requests, not at build time. This assumes no cacheComponents or static export. Compile and controlled local transport checks do not certify hosted availability or remote workspace admission.
Implement public read and customer participation
Copy all three files. The feed checks emptiness before choosing a post and handles nullable detail separately. Only public projected fields are rendered. The Server Action reads identity before creating a comment and performs a separate public comment lookup. New comments default to published/approved; this is not a pre-moderation queue. A subsequent moderation change or the bounded lookup page can keep a created comment out of that readback; report what the read actually observed.
// app/community/page.tsx
import { createClient } from '@01.software/sdk'
export const dynamic = 'force-dynamic'
export default async function CommunityPage() {
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
apiUrl: process.env.SOFTWARE_API_URL,
})
const feed = await client.community.listPosts({ limit: 10 })
if (feed.docs.length === 0) return <p>No public posts yet.</p>
const post = await client.community.getPost({
postId: String(feed.docs[0].id),
})
if (!post) return <p>This post is no longer available.</p>
const comments = await client.community.listComments({
postId: String(post.id),
limit: 10,
})
return (
<main>
<h1>{post.title}</h1>
{comments.docs.length === 0 ? (
<p>No public comments yet.</p>
) : (
<ul>
{comments.docs.map((comment) => (
<li key={comment.id}>{comment.body}</li>
))}
</ul>
)}
<a href={`/community/${encodeURIComponent(String(post.id))}`}>
Join the conversation
</a>
</main>
)
}// app/community/[postId]/actions.ts
'use server'
import { currentCustomer } from '@/lib/customer'
export async function addComment(postId: string, form: FormData) {
const { client, customer } = await currentCustomer()
if (!customer) return { status: 'sign_in_required' }
const body = String(form.get('body') ?? '').trim()
if (!body || body.length > 1000) return { status: 'invalid_comment' }
// Author/tenant/moderation fields are never supplied by the app.
const created = await client.community.createComment({ postId, body })
const comments = await client.community.listComments({
postId,
limit: 10,
sort: '-createdAt',
})
return {
status: comments.docs.some((comment) => comment.id === created.id)
? 'visible'
: 'submitted',
}
}// app/community/[postId]/page.tsx
import { createClient } from '@01.software/sdk'
import { notFound } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import { addComment } from './actions'
export const dynamic = 'force-dynamic'
export default async function Conversation({
params,
}: {
params: Promise<{ postId: string }>
}) {
const { postId } = await params
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
apiUrl: process.env.SOFTWARE_API_URL,
})
const post = await client.community.getPost({ postId })
if (!post) return notFound()
const comments = await client.community.listComments({
postId,
limit: 10,
sort: '-createdAt',
})
async function submit(form: FormData) {
'use server'
const result = await addComment(postId, form)
if (result.status === 'sign_in_required') throw new Error('Sign in first')
if (result.status === 'invalid_comment')
throw new Error('Write a comment of 1–1000 characters')
revalidatePath(`/community/${encodeURIComponent(postId)}`)
}
return (
<main>
<h1>{post.title}</h1>
{comments.docs.length === 0 ? (
<p>No public comments yet.</p>
) : (
<ul>
{comments.docs.map((comment) => (
<li key={comment.id}>{comment.body}</li>
))}
</ul>
)}
<a href="/account">Account</a>
<form action={submit}>
<textarea name="body" required maxLength={1000} aria-label="Comment" />
<button>Submit comment</button>
</form>
</main>
)
}Normal, empty, and failure results
Open /community with zero posts, then one visible post. A post removed between feed and detail becomes the unavailable state. Open its conversation, sign in through /account, submit one comment, and verify it through listComments() and a fresh page load. An empty comment list is distinct from a failed request. The bounded first page may not include the new comment; submitted is not proof of rejection or publication. Read back the appropriate page or ask moderation support before repeating a write.
Missing, hidden/rejected by moderation and foreign-workspace post detail all return null only for the canonical not_found response. An unrelated deployment 404, auth, workspace, feature or network error must propagate. Add app read/action error UI; never turn every exception into an empty feed. Check unsigned and foreign-workspace writes, hidden/rejected post, banned customer, locked post and disabled comments. Denial must leave no new comment; a successful HTTP response alone is not a readback.
The server derives author/profile and workspace from the verified customer; the app never supplies moderation, author, counters or provenance fields. A parent/reply must belong to the same post and stay within nesting limits. Raw posts, comments, reactions, reports and bans are server-only collections, not browser CRUD alternatives. A customer cannot acquire moderation power by using a different client.
Next Actions
Use Content Operations for moderation and support, SDK for reactions and bookmarks, and Deploy for browser origins and hosted session checks. On logout, distinguish app cookie deletion from hosted server revocation; see Customers. Remote workspace/denial/readback/cleanup and hosted journeys remain pending until observed against the same staging SHA.