The [hash] Route & LINE OA Resolution
Overview
Every client-facing mini-app lives under a path shaped like /:hash/..., where hash is the public identifier of a LINE OA (the same value as the webhook key hash). This value is the key the web app uses to know which OA context it is operating in.
Pages use the hash to resolve four things:
lineLiffId, needed to initialize LIFFformLiffId, a separate LIFF for form pages when one is configuredbotBasicIdin@xxxxform, used to build links into the OA's chat room- The OA's display name and image, used to build Open Graph metadata when links are shared
Beyond resolving data, the layout for this segment (src/app/[hash]/layout.tsx) also generates OA-level link previews server-side for every page beneath the path.
Business Flow
- The user opens a URL shaped like
/:hash/:feature/.... - Server side: the layout's
generateMetadata()calls the OA lookup endpoint, trying the internal API URL first and falling back to the public URL, with a 300-second cache. Once it has the OA's name and image, it assemblestitle,description,openGraph, andtwittercard metadata. If the call fails, it falls back to values from environment variables. - Client side: the page calls
useFetchGetLineOaByHash(hash)through React Query, configured with a single retry. - The resolved
lineLiffIdis stored inuseAppStoreand passed touseLiffAuthto begin the login process. - Child pages may declare their own metadata; Next.js merges child values over parent values automatically.
- The
/:hashpath — the OA index page — acts as a simple authorization check: it logs in via LIFF and displays the user profile, returning a Not Found page if no profile is available.
Key Screens & Components
Layout and index page
- The segment layout (
src/app/[hash]/layout.tsx) contains bothgenerateMetadata()and the server-side OA fetch helper. - The index page (
src/app/[hash]/page.tsx) is the auth check and profile display screen.
OA resolution
useFetchGetLineOaByHash(hash)(src/hooks/use-fetch-line-oa-by-hash.ts) is the client-side hook.- The LINE OA service (
src/service/line-oa.service.ts) supports both the current response shape that returnslineLiffIddirectly and the legacy shape where it is nested under a login-info object. - The endpoint used is
GET /line-oa/get-by-hash/:hash. - Relevant environment variables:
BASE_API_INTERNAL_URL,NEXT_PUBLIC_BASE_API_CLIENT_URL,NEXT_PUBLIC_APP_URL,NEXT_PUBLIC_SEO_TITLE, andNEXT_PUBLIC_SEO_DESCRIPTION.
All routes under this segment
| Route | Feature |
|---|---|
/:hash | Auth check and profile display |
/:hash/form | Catch page that receives liff.state and redirects to the form |
/:hash/form/:id | Form filling |
/:hash/form/:id/thank-you | Post-submission thank-you page |
/:hash/content/:token | Article viewer |
/:hash/pages/:linkToken | Content listing driven by a content link |
/:hash/menu/:token | Menu built with the menu builder |
/:hash/friend-track | Friend-add campaign, resolved from the hash |
/:hash/friend-track/:token | Friend-add campaign identified by token |
/:hash/bulletin | Bulletin board |
/:hash/bulletin/:postId | Bulletin post detail |
/:hash/loyalty | Customer-facing loyalty card |
/:hash/loyalty-staff | Staff tooling |
/:hash/booking/:token | Appointment booking |
/:hash/r/:id | Tracking redirect |
Routes outside this segment
The home page (/), the Not Found page, the verify-line-login page, the campaign redirect proxy at /c/:token, the Ant Design sandbox pages, and the Sentry example API route.
Dependencies
- Directly coupled to LINE Login via LIFF, since every page begins by resolving its LIFF ID through this segment.
generateMetadata()uses Next.jsfetchrather than axios so that a revalidate interval can be specified.formLiffIdis consumed only by Form Filling and the Post-submission Thank-you Page.botBasicIdis consumed by the Post-submission Thank-you Page and the Friend-add Tracking Campaign.
Backend Details (Client API)
GET /api/line-oa/get-by-hash/:hash is the first endpoint every LIFF page calls, and the only route deliberately designed to require no authentication at all — the web app has no LIFF ID yet, so it cannot run liff.init() to obtain a token. This endpoint breaks that chicken-and-egg problem.
A lookup rule that explains "resolves fine but login fails"
This detail matters and is a common source of confusion: the backend looks up the OA with a condition looser than the token verification step.
- This endpoint accepts any OA that has not been deleted; it does not require active status.
- But the token verification described in LINE Login via LIFF requires the OA to be active.
- The result: a disabled (inactive) OA will resolve its hash successfully, hand the web app a LIFF ID, and let
liff.init()succeed — then every real API call returns 401Invalid channel. It looks like an auth problem, but the root cause is the OA's status. - A hash that does not exist returns 400 (not 404) with the message code
APP_007, kept for parity with the legacy system. The web app should therefore treat a 400 from this endpoint as "OA not found".
Response shape, and why several fields can be null
lineLiffIdandformLiffIdare read from the OA's LINE Login configuration, where an empty value or a missing key becomesnull, not an empty string — the web app must check for null rather than relying on string truthiness alone.formLiffIdbeingnullis normal (an OA with no separate form LIFF configured); in that case the form page must fall back to the primary LIFF ID.imageUrlis derived by resolving the cover image's storage path into a public URL, with careful rules already in place: an empty path yieldsnull(rather than a URL pointing at an empty bucket root, which would break og:image), a value that is already an absolute URL is passed through unchanged, and if storage is not configured the field isnullwithout raising an error.botBasicIdis the public@-id, used to build the OA chat link in the formhttps://line.me/R/ti/p/<id>, which is required when the thank-you page is opened in an external browser (outside the LINE app).nameis the OA name the web app uses as the document title and OG title.
Security notes
- This endpoint is public by design, so it returns only genuinely safe fields — every value it exposes already appears on the OA's public LINE page.
- No credential (channel secret, access token, internal configuration) may ever be added to this response. If the web app needs more data, it must go through a separate token-verified endpoint.
- A guessable hash is not a vulnerability in itself, because every route carrying real data still enforces channel binding on top.
Edge cases worth knowing
- The backend does not cache this endpoint — the 300-second cache described earlier in this document belongs to Next.js
generateMetadata()only. So when an admin changes the OA name or cover, client-side pages see the new value immediately, while OG metadata may lag until the cache expires. - The same OA record is also resolved by other features through their own domain-level hash lookups (bulletin board, loyalty card), which may apply slightly different OA status conditions than this endpoint does.