Skip to main content

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.

"Board = Site-wide setting"

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.

CategoryBuiltin BoardExternal Board Plugin (free bulletin board)
Introducedv1.37.0 (Phase 3)Separate plugin
PurposeNotice / FAQ / 1:1 inquiryFree bulletin board (posting, comments, likes, scraps)
ModelsApp\Models\Board, BoardPost, BoardInquiry, BoardInquiryReplyApp\Plugins\Board namespace models
DB tablessite_board_*plg_board_*
Filament ResourceBoardResource (slug site-boards)BoardResource (slug boards)
Activation methodBuiltin by default (CORE_BOARD_ENABLED)Operator-selectable plugin
Operating partySaaS (L1) / Tenant (L2)Tenant with the plugin enabled
Reason for separating tables/slugs

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 constantValueDescriptionContained items
Board::KIND_NOTICEnoticeNoticesBoardPost
Board::KIND_FAQfaqFrequently asked questions (category groups)BoardPost
Board::KIND_INQUIRYinquiry1:1 inquiry intakeBoardInquiry
Board::KIND_GENERICgenericGeneric board (freely created)BoardPost

Audience scope (audience_scope)​

Each container uses audience_scope to define "who can see it."

scope constantValueAudience
Board::SCOPE_SAASsaasThe entire SaaS (all tenants, regardless of tenant_id)
Board::SCOPE_TENANTtenantThe entire corresponding Tenant
Board::SCOPE_ORGorgThe 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 β€” automatic saas_product_id assignment + SaaS isolation
  • BelongsToTenant β€” automatic tenant_id assignment + Tenant isolation
  • Auditable β€” 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.

RLS policies (v1.37.1 correction)

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.

MigrationTableModel
2026_05_31_000002_create_site_board_boards_tablesite_board_boardsBoard
2026_05_31_000003_create_site_board_posts_tablesite_board_postsBoardPost
2026_05_31_000004_create_site_board_inquiries_tablesite_board_inquiriesBoardInquiry
2026_05_31_000005_create_site_board_inquiry_replies_tablesite_board_inquiry_repliesBoardInquiryReply
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)β€”
Handling production 500 errors

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).

keykindNameaudience_scope
noticenoticeNoticestenant
faqfaqFAQtenant
inquiryinquiry1:1 Inquirytenant

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.

PanelMenunavigation groupSlug/path
SaaSBoard (BoardResource)Site Management/saas/site-boards
SaaS1:1 Inquiry (InquiryResource)Customer Inquiry/saas/{inquiry-slug}
TenantBoard / 1:1 InquirySite Management / Customer Inquiry/tenant/.../site-boards
OrgBoard / 1:1 InquirySite Management / Customer Inquiry/org/site-boards
  • BoardResource is in the Site Management navigation group, with slug site-boards (separated from the external Board plugin's boards).
  • InquiryResource is displayed in the Customer Inquiry navigation 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 classMenuRole
App\Filament\App\Pages\NoticesPageNoticesView notice posts exposed to one's own hierarchy
App\Filament\App\Pages\FaqPageFAQView FAQ posts grouped by category
App\Filament\App\Pages\MyInquiriesPage1:1 InquiryCreate, 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 keyLabelcustom_actions
boardsBoard Managementmanage_notice (manage notices), manage_faq (manage FAQ), reply (write replies)
inquiriesInquiry Managementreply (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.

  • 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