Skip to main content

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:

  1. lineLiffId, needed to initialize LIFF
  2. formLiffId, a separate LIFF for form pages when one is configured
  3. botBasicId in @xxxx form, used to build links into the OA's chat room
  4. 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

  1. The user opens a URL shaped like /:hash/:feature/....
  2. 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 assembles title, description, openGraph, and twitter card metadata. If the call fails, it falls back to values from environment variables.
  3. Client side: the page calls useFetchGetLineOaByHash(hash) through React Query, configured with a single retry.
  4. The resolved lineLiffId is stored in useAppStore and passed to useLiffAuth to begin the login process.
  5. Child pages may declare their own metadata; Next.js merges child values over parent values automatically.
  6. The /:hash path — 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 both generateMetadata() 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 returns lineLiffId directly 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, and NEXT_PUBLIC_SEO_DESCRIPTION.

All routes under this segment

RouteFeature
/:hashAuth check and profile display
/:hash/formCatch page that receives liff.state and redirects to the form
/:hash/form/:idForm filling
/:hash/form/:id/thank-youPost-submission thank-you page
/:hash/content/:tokenArticle viewer
/:hash/pages/:linkTokenContent listing driven by a content link
/:hash/menu/:tokenMenu built with the menu builder
/:hash/friend-trackFriend-add campaign, resolved from the hash
/:hash/friend-track/:tokenFriend-add campaign identified by token
/:hash/bulletinBulletin board
/:hash/bulletin/:postIdBulletin post detail
/:hash/loyaltyCustomer-facing loyalty card
/:hash/loyalty-staffStaff tooling
/:hash/booking/:tokenAppointment booking
/:hash/r/:idTracking 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

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 401 Invalid 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

  • lineLiffId and formLiffId are read from the OA's LINE Login configuration, where an empty value or a missing key becomes null, not an empty string — the web app must check for null rather than relying on string truthiness alone.
  • formLiffId being null is normal (an OA with no separate form LIFF configured); in that case the form page must fall back to the primary LIFF ID.
  • imageUrl is derived by resolving the cover image's storage path into a public URL, with careful rules already in place: an empty path yields null (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 is null without raising an error.
  • botBasicId is the public @-id, used to build the OA chat link in the form https://line.me/R/ti/p/<id>, which is required when the thank-you page is opened in an external browser (outside the LINE app).
  • name is 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.