Getting Started with the Builtin Board
The Builtin Board is a lightweight notice / FAQ / 1:1 inquiry module added in v1.37.0. It operates on top of the site_board_* tables, allowing SaaS and Tenant operators to create boards themselves to post notices to members or to respond to inquiries.
The Builtin Board is not a per-member feature but a site operation setting. Board containers (notice / FAQ / inquiry) are created by SaaS (L1) and Tenant (L2) operators, while members read the posts that are exposed to them or submit inquiries.
Difference from the External Board Pluginβ
The Builtin Board is completely separate from the external Board plugin (App\Plugins\Board, a free bulletin board). Despite the similar names, the code, tables, and purpose are all different.
| Category | Builtin Board | External Board Plugin (free bulletin board) |
|---|---|---|
| Introduced | v1.37.0 (Phase 3) | Separate plugin |
| Purpose | Notice / FAQ / 1:1 inquiry | Free bulletin board (posting, comments, likes, scraps) |
| Models | App\Models\Board, BoardPost, BoardInquiry, BoardInquiryReply | App\Plugins\Board namespace models |
| DB tables | site_board_* | plg_board_* |
| Filament Resource | BoardResource (slug site-boards) | BoardResource (slug boards) |
| Activation method | Builtin by default (CORE_BOARD_ENABLED) | Operator-selectable plugin |
| Operating party | SaaS (L1) / Tenant (L2) | Tenant with the plugin enabled |
Because the external Board plugin already occupies the board_* tables (more precisely plg_board_*) and the boards slug, the Builtin Board separates its table prefix to site_board_ and its Filament slug to site-boards to avoid conflicts. Each model explicitly declares its $table property.
The Board Container Conceptβ
The top-level unit of the Builtin Board is the board container (App\Models\Board). A container's type is determined by its kind, and it holds posts (BoardPost) or inquiries (BoardInquiry).
| kind constant | Value | Description | Contained items |
|---|---|---|---|
Board::KIND_NOTICE | notice | Notices | BoardPost |
Board::KIND_FAQ | faq | Frequently asked questions (category groups) | BoardPost |
Board::KIND_INQUIRY | inquiry | 1:1 inquiry intake | BoardInquiry |
Board::KIND_GENERIC | generic | Generic board (freely created) | BoardPost |
Audience scope (audience_scope)β
Each container uses audience_scope to define "who can see it."
| scope constant | Value | Audience |
|---|---|---|
Board::SCOPE_SAAS | saas | The entire SaaS (all tenants, regardless of tenant_id) |
Board::SCOPE_TENANT | tenant | The entire corresponding Tenant |
Board::SCOPE_ORG | org | The audience_org_id organization and its descendant subtree (regardless of depth) |
The boards visible to a member are determined by the model scope Board::scopeVisibleToMember(). It combines SaaS-wide notices, notices for the Tenant the member belongs to, and org notices that fall within the member's organization ancestor chain (ViewerScopeResolver::orgAncestorIds()).
Who creates boardsβ
Because creating/deleting a board container is a "site-wide setting," only L1 SaaS and L2 Tenant operators can do it. L3 organization operators can only manage posts and respond to inquiries within their own subtree boards (creation not allowed). This rule is enforced in BoardResource::canCreate() (level <= 2) and Board::scopeManageableBy().
Two-tier Isolation + RLSβ
All four models (Board, BoardPost, BoardInquiry, BoardInquiryReply) implement SaasTenantOwnedModelInterface and use the following traits.
BelongsToSaasProductβ automaticsaas_product_idassignment + SaaS isolationBelongsToTenantβ automatictenant_idassignment + Tenant isolationAuditableβ automatic audit logging of create/update/delete
In other words, data is isolated across two tiers: SaaS + Tenant. In addition to the application-level global scopes, PostgreSQL Row Level Security (RLS) policies isolate rows based on the saas_product_id / tenant_id session variables.
The initial RLS policies had an issue where they rejected seed/migration INSERTs due to a missing WITH CHECK and the absence of a session-variable NULL bypass. The 2026_06_01_000001_fix_site_board_rls_policies migration in v1.37.1 replaced them with the demo_posts pattern (saas/tenant separated policies + USING + WITH CHECK + a bypass when the session variable is NULL / '' / '0'). Databases that have already been migrated are corrected forward-only to avoid the risk of rolling back the same batch.
Migrations / Tablesβ
It consists of 5 site_board_* migrations + 1 RLS correction in database/migrations/. All of them apply a Schema::hasTable() idempotency guard and a data-preserving down() (deferred drop), making them safe for backporting.
| Migration | Table | Model |
|---|---|---|
2026_05_31_000002_create_site_board_boards_table | site_board_boards | Board |
2026_05_31_000003_create_site_board_posts_table | site_board_posts | BoardPost |
2026_05_31_000004_create_site_board_inquiries_table | site_board_inquiries | BoardInquiry |
2026_05_31_000005_create_site_board_inquiry_replies_table | site_board_inquiry_replies | BoardInquiryReply |
2026_05_31_000006_setup_site_board_rls_policies | (RLS policies) | β |
2026_06_01_000001_fix_site_board_rls_policies | (RLS correction, v1.37.1) | β |
The relation "site_board_boards" does not exist error is not a code problem but a state in which the migration has not been applied to the production DB. Resolve it with make migrate NAME={project}.
Enable / Disableβ
The Builtin Board ships enabled by default. It is controlled by the board block in config/core.php.
// config/core.php
'board' => [
'enabled' => env('CORE_BOARD_ENABLED', true),
'tables' => [
'boards' => 'site_board_boards',
'posts' => 'site_board_posts',
'inquiries' => 'site_board_inquiries',
'replies' => 'site_board_inquiry_replies',
],
'kinds' => ['notice', 'faq', 'inquiry', 'generic'],
'inquiry' => [
// Allow escalating an unresolved inquiry one level up to the parent org
'escalation_enabled' => env('CORE_BOARD_INQUIRY_ESCALATION', true),
],
],
# .env (default values)
CORE_BOARD_ENABLED=true # Builtin Board global on/off
CORE_BOARD_INQUIRY_ESCALATION=true # Whether to expose the inquiry escalation action
Both the Filament Resources and the member pages use config('board.enabled', true) as a gate, so disabling it by setting it to false hides both the operator menus and the member menus.
Default Seed (CoreSeeder)β
CoreSeeder::createDemoBoards() idempotently seeds 3 default boards (all with is_system=true) per tenant (using updateOrCreate keyed on tenant_id + key). It is skipped if board.enabled=false or if the site_board_boards table does not exist (backport-safe).
| key | kind | Name | audience_scope |
|---|---|---|---|
notice | notice | Notices | tenant |
faq | faq | FAQ | tenant |
inquiry | inquiry | 1:1 Inquiry | tenant |
In addition, the demo data seeds 1 notice post ("Welcome", pinned), 1 FAQ post ("I forgot my password", category Account), and 1 inquiry ("Service usage inquiry") written by the member@ member. The member inquiry assigns the organization that member@ belongs to (or the HQ organization if none) as organization_id and assigned_org_id, so that the hierarchical scenario where the org@ operator responds can be demonstrated right away.
Access Pathsβ
Operators (Filament admin panel)β
The Builtin Board Filament plugin (BuiltinBoardFilamentPlugin, id builtin-board) is registered on the SaaS, Tenant, and Org panels. It is not registered on the App panel.
| Panel | Menu | navigation group | Slug/path |
|---|---|---|---|
| SaaS | Board (BoardResource) | Site Management | /saas/site-boards |
| SaaS | 1:1 Inquiry (InquiryResource) | Customer Inquiry | /saas/{inquiry-slug} |
| Tenant | Board / 1:1 Inquiry | Site Management / Customer Inquiry | /tenant/.../site-boards |
| Org | Board / 1:1 Inquiry | Site Management / Customer Inquiry | /org/site-boards |
BoardResourceis in theSite Managementnavigation group, with slugsite-boards(separated from the external Board plugin'sboards).InquiryResourceis displayed in theCustomer Inquirynavigation group.- Because the board menu is a "site-wide setting," it is only exposed in a manageable site context (an independent site or the SaaS default site). It is not shown on mirror sites (which share the Platform default settings) (
SiteTypeHelper::currentContextIsManageableSite()).
Members (App panel)β
The 3 member pages are automatically registered (discoverPages) on the App panel (/app).
| Page class | Menu | Role |
|---|---|---|
App\Filament\App\Pages\NoticesPage | Notices | View notice posts exposed to one's own hierarchy |
App\Filament\App\Pages\FaqPage | FAQ | View FAQ posts grouped by category |
App\Filament\App\Pages\MyInquiriesPage | 1:1 Inquiry | Create, view, and reply to one's own inquiries |
All three pages are exposed in the menu only when config('board.enabled', true) is true.
Permission Catalogβ
Sub-admins (admins created through the form under a Chief) must have per-menu permissions to gain access (enforced since v1.38.0). Primary admins (chief_id=null) and Level 0 have full authority. The following feature keys are defined in config/permissions.php.
| feature key | Label | custom_actions |
|---|---|---|
boards | Board Management | manage_notice (manage notices), manage_faq (manage FAQ), reply (write replies) |
inquiries | Inquiry Management | reply (write answers), assign (assign an owner) |
The feature key for BoardResource is boards, and for InquiryResource it is inquiries. Exposure/access is an AND merge of config('board.enabled') + site context + the read permission for that key.
Related Documentsβ
- For operational procedures, see the Administrator Guide.
- Design background:
workspace/_docs/design/03_features/23_board-commerce-plugins.md,workspace/_docs/design/01_decisions/10_board-commerce-strategy.md