Rich menu default
Overview
Rich menu default is the LINE OA's baseline menu — the image menu pinned below the chat screen — split into a version for guests and one for members, with exactly one of each allowed. The thing to understand up front: saving here doesn't just write a database row. It fires a sequence of calls against the real LINE Messaging API, and if any step in that sequence fails the menu is stamped failed with the failure reason stored alongside it.
Who uses it — admins setting up the main menu everyone chatting with the OA sees (this module carries a real, enforced ModuleGate on the backend — see Access control below).
User journey
1. Review the default menus
Open Rich menu > Rich menu default. This page has no filter bar at all (unlike Rich menu custom and Switch rich menu), since at most 2 records can ever exist. The table shows No. / Image / Title / Type / Period / Status / Action — the "Period" column always renders - for a default menu, because guest/member menus have no start/end dates (that column exists for the custom type).
The "Create rich menu" button hides itself once both guest and member exist, and this page has no delete button — only view (eye) and edit (pencil).
2. Create a default menu
- Click create, opening the form at
/rich-menu/form?type=default. - Enter a title — must not duplicate another rich menu in the same OA (checked on the backend).
- Pick the type — Guest or Member. The system reads the existing records first: if a member menu already exists, the Member option is disabled and Guest is selected automatically (and vice versa). This form has no status field at all — the API stores it as active regardless.
- Pick a layout from 15 options across 3 tabs — large (1–8), compact (9–13), custom (14–15). The pre-selected default is layout 8 (a single full-image tap area), not layout 1. Changing tab or layout clears the current image and every tap area.
- Upload a background image — JPG/JPEG/PNG up to 1 MB, and the pixel dimensions must match an exact allow-list: large / full-custom layouts (1–8, 14) accept
2500×1686,1200×810,800×540; compact layouts (9–13, 15) accept2500×843,1200×405,800×270. Anything else is rejected before the upload happens. - Set the menu bar label — "Menu (Default)" or a custom label up to 14 characters.
- Configure the action per cell (labeled A, B, C…) — 5 types: Link (URL must start with
https://, up to 500 characters, with a "Tracking" checkbox), Text (up to 300 characters), Phone (up to 10 digits, validated against the shared Thai phone-number rule), Switch rich menu (pick a submenu from Switch rich menu attached to this menu), or No action. Every type has a required "Action Label" capped at 20 characters — the same cap LINE itself enforces. - Save. A confirmation dialog appears first, then the system calls
POST /api/rich-menuas multipart/form-data.
3. Review click stats
- From the list, click the eye icon to open view mode — every field is read-only and a live preview of the menu appears.
- Below it is a per-cell stats table — Spot (rendered as A, B, C…), Data (the label; for link-type spots it's clickable and opens the original destination URL), Action type, Total click, Unique click (the last two are sortable).
- A note reads "Click stats refresh automatically every hour," next to a Re-sync button — that button calls
POST /api/rich-menu/{id}/recalculate-stat, which queues a job rather than computing live; the page waits 2.5 seconds and refetches on its own.
4. Edit an existing menu
Click the pencil to edit — the status field is hidden entirely on a default menu's edit page. Saving over it creates a brand-new rich menu on LINE and deletes the old one (see below).
Key details verified against the source
Confirmed from internal/modules/richmenu/{controller,service,dto,create_update,update,archive_stats}.go and internal/modules/linemessageapi/{richmenu,layout}.go in line-management-cms-api-go:
- Saving runs a 6-step LINE sequence, and only when the status actually stored is
active— (1) read width/height out of the uploaded image buffer, (2)POSTthe rich menu to LINE with a{size, selected, name, chatBarText, areas}payload, (3) write theline_rich_menu_idLINE returns back to the database, plus a snapshot row inrich_menu_archive, (4) upload the image onto that rich menu, (5) create the rich menu alias, (6) only if the type is guest, set it as the OA's default rich menu. Then clear the Redis cache and queue a stat-calculation job. If any step fails, the row is writtenstatus = failedwith the reason in areasoncolumn and the API answers 400"createRichMenuOnLineOA : Fail". - A member menu isn't bound to any user at all on create — step 6 above checks for guest only. Binding a member menu to every member happens only on edit, where the backend publishes a
setRichMenuMemberjob onto theline_change_richmenuqueue for the worker to fetch everyline_userwithuser_type = memberand link them to LINE in batches of 500. In practice a freshly created member menu usually needs a second save before members actually see it. - Every save creates a new rich menu on LINE and then deletes the old one —
line_rich_menu_idchanges each time. The alias, meanwhile, is derived from a fixed formula —richmenu-alias-+ the first 16 chars ofmd5("{organizationId}_{lineOaId}_{richMenuId}")— so it always resolves to the new menu with nothing else to update (edits try to update the alias first, falling back to create if that fails). And because a freshrich_menu_archiverow is written on every save, each published version stays on record for stats to attach to. - Layout 15 (custom–compact) publishes zero tap areas to LINE — the backend's layout table declares both 14 and 15 with
TotalActions: 0and no fixed bounds, but the code that derives areas from a user-drawn imagemap is gated onlayoutNo == 14only. Layout 15 therefore falls through to the table'sTotalActions: 0, sending LINE an emptyareasarray — the drawn areas are still saved in the database and still appear in the stats table, but tapping them in the real LINE app does nothing. Use layout 14 (custom–full) for a hand-drawn menu instead. - A "No action" cell is dropped from the LINE payload entirely — not sent as an area with no action, but omitted from
areasaltogether, making that part of the image genuinely dead space. - Click tracking works completely differently per action type — a Link with "Tracking" checked has its destination URL swapped for a freshly minted short redirect link carrying metadata (
contentType: rich_menu,organizationId,lineOaId,richMenuId,richMenuActionIndex,richMenuArchiveId) before being sent to LINE; leave it unchecked and the original URL goes to LINE verbatim and produces no click data whatsoever. A Text cell is converted to a LINEpostbackaction (notmessage), and a Switch rich menu cell torichmenuswitch— both of which record through the webhook rather than a redirect link. A Phone cell goes out as a plaintel:URI with no tracking at all. - Stats only refresh automatically for menus whose tracked links get clicked — the hourly cron picks up only menus flagged
RICH_MENU_STAT_DIRTY:{id}in Redis, and the only writer of that flag is the redirect service that receives tracked-link clicks. Postback / switch clicks (which arrive via the webhook) are written totracking_logbut never set the flag — so a menu built entirely from Text / Phone / Switch cells never gets recalculated on its own and needs the Re-sync button. - A default menu's stats accumulate across every republish, with a guard against the numbers going misleading when the layout changes —
template_trackinghangs off the stablerich_menu.id, not off each archive, so repurposing a cell would blend old counts into the new meaning. The read path therefore compares the two most recent archives (by index + type + link) and, when the layouts differ, computes stats live scoped to the latest archive only and returns that instead — never writing it back. - The backend's status-transition rules apply to type
switchonly —ValidateStatusTransition/ValidateStatusForAction/ValidateStatusForDeleteall open withif type == switch. The status rules for guest/member (and custom) menus are enforced on the frontend alone. - The backend's image size cap is 10 MB, not 1 MB — the DTO validates the mime type (
image/jpeg/image/png) and a10,000,000-byte limit. The 1 MB rule and the pixel-dimension allow-list are frontend-only; the backend never checks resolution.
Related menus in this journey
- Switch rich menu — this default menu must exist before a submenu can be attached to it, and existing submenus become the options for a "switch rich menu" action in this form (the dropdown filters on
parentIdequal to this menu and keeps onlyactiveones). The form also has a "Create new" button that opens the Switch rich menu form in a new tab withparentIdalready passed through. - Rich menu custom — the same machinery end to end (one table, one module, one form); only the record type, the date window, and the audience differ.
- Segment audience — not involved in default menus directly, but required for Rich menu custom.
- Workflow / Trigger rule — can switch a specific user's rich menu through the
line_change_richmenuqueue as asetRichMenuByTriggerRulejob.
Things to watch out for
- A newly created member menu has no effect on members until you save it a second time (see above) — if members still see the old menu, open it and save again.
- Avoid layout 15 until it's fixed — the tap areas you draw won't work in the real LINE app.
- A link without "Tracking" checked yields no stats at all, not "stats that happen to be zero" — the row simply sits at 0 forever.
- Stats for a menu with no tracked links never update on their own, even with real taps — you have to use "Re-sync." (The on-screen "refreshes automatically every hour" note is only true for menus flagged by a tracked-link click.)
- Saving takes several seconds because it talks to LINE 3–4 times (create / upload image / manage alias / set as default), and a failure mid-sequence leaves a
failedstatus — saving again is treated as a retry and pushes the status back toactivefor you. - The 1 MB file size and the pixel-dimension check are frontend rules only; calling the API directly accepts up to 10 MB and any dimensions (though LINE may reject them later).
Access control (verified against the source)
- Frontend — this page is wrapped in a
PermissionGuardchecking theVIEWaction on therich-menu-defaultsubject, a separate subject fromrich-menu-customandrich-menu-switch. - Backend — every route in the module is wrapped in a real, enforced
ModuleGate("rich-menu"), and it is a single gate covering all three types at once (Default / Custom / Switch all live in the samerich_menutable, distinguished only by atypecolumn) — if a platform admin closes therich-menumodule for an organization, all three pages 403 together; there's no way to close just one type at the backend level (onemoby-type callers bypass the gate). - Worth knowing — frontend permissions split three ways (default/custom/switch) while the backend has a single layer, so hiding a menu in the UI does not close off that type's API.
- Action-level policy annotations (
create/update/delete/read/readAll) are all declared but run through the same no-op middleware as most other modules — the real enforcement is the module-level ModuleGate. - Cross-tenant (IDOR) protection lives at the query level —
findByIdalways adds the caller'sline_oa_id(a miss returns 400APP_007),recalculate-statverifies ownership before queueing (a miss returns 404), and every delete statement is scoped byline_oa_idtoo. DELETE /api/rich-menu/:idis a soft delete (status todelete, then stampdeleted_date); a true hard delete (DELETE /api/rich-menu/:id/hard) requires Super Admin and aconfirm=truequery parameter, or it's rejected.
Source file reference
Frontend (cms-web-2026)
src/app/rich-menu/default/page.tsx— list entry point (PermissionGuardsubjectrich-menu-default)src/app/rich-menu/form/page.tsx— the form entry point shared by all three types (readstypeoff the query string)src/components/rich-menu/list/rich-menu.container.tsx— table/columns per type, edit-and-delete disable rules, hiding the create button once guest+member both existsrc/components/rich-menu/form/default/rich-menu-default-form.container.tsx— FormData assembly, the 1 MB rule, stat re-sync,getCreateStatussrc/components/rich-menu/form/template/—template-layout.tsx(15 layouts / 3 tabs),template-content.tsx,template-custom-area.tsx(20-area cap)src/components/rich-menu/form/action-setting/—action-setting.tsx(the 5 action types),action-setting-render-type.tsx(per-type rules)src/components/rich-menu/form/utils/upload-file.ts— file size and per-layout resolution rulessrc/services/rich-menu.service.ts— every endpoint the frontend callsmessages/{th,en}/rich-menu.json— all copy and labels for all three pages
Backend (line-management-cms-api-go)
internal/modules/richmenu/controller.go— all 8 routes,ModuleGate("rich-menu"), hard-delete'sSuperAdminGuardinternal/modules/richmenu/create_update.go— the LINE call sequence on create,effectiveCreateStatus, archive writesinternal/modules/richmenu/update.go— the edit sequence, deleting the old menu on LINE, thefailed-menu retry mechanisminternal/modules/richmenu/service.go—findById(scoped byline_oa_id),RecalculateStat, switch-only status rules, deleteinternal/modules/richmenu/archive_stats.go— live stat computation when the layout changed between archivesinternal/modules/linemessageapi/richmenu.go— action-to-LINE-payload conversion and tracked-link creationinternal/modules/linemessageapi/layout.go— the 15-layout table and image sizes
Worker (line-management-worker-go)
internal/cronscheduler/rich_menu_stat_scanner.go— the hourly cron that picks up only flagged menusinternal/richmenu/service_methods.go—setRichMenuMemberByLineOaId(links a member menu 500 users at a time)
See the documentation team's technical write-up at Rich Menu (Default / Custom / Switch)