---
path: /app/[surface]/[[...path]]
surface: workspace
domain: workspace (canonical shell-route alias / redirect)
auth: anon (redirect-only; downstream route enforces auth)
source:
  apps/oshun/web/src/proxy.ts;
  apps/oshun/web/src/app/app/[surface]/[[...path]]/page.tsx
status: walked
last_walked:
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; live
  screen-reader, touch, offline, and telemetry-delivery checks pending a manual
  AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content
  re-verified 2026-06-03 against current source; alias redirect contract
  re-verified 2026-06-30 with apps/oshun/web/e2e/canonical-deep-links.spec.ts
  and middleware-public-paths unit coverage'
---

# App canonical-shell redirect

## Purpose

A redirect-only catch-all that maps `/app/<surface>/<rest>` to the canonical
shell route for `<surface>`. Used as a URL-stable alias so external links, push
notifications, deep links, and standalone-PWA launches can reference
`/app/<surface>` without baking in the canonical path (which may evolve). The
proxy resolves the alias before the auth gate so the alias itself stays
anonymous-accessible; the canonical downstream route still applies its own auth
and shell policy. The page component remains as the route-level fallback and
returns `null` after issuing the same redirect.

## Entry points

- **PWA `start_url`** (per `manifest.json`) routes to `/?surface=pwa`, but
  `/app/[surface]` is the canonical-alias form used in deep links outside the
  app shell
- **Push notification deep links** with surface-stable targets
- **Marketing site** linking into specific shell surfaces by name
- **`/app/search/...` rewrites** — `params.surface === 'search'` maps to
  `'explore'` (search lives inside Explore)
- **Email / SMS deep links** referencing surface by name rather than path

## Layout regions

_None._ The page returns `null` after issuing a server-side `redirect()`.

## States

- [x] **Valid surface** — `params.surface` matches `isWebShellRoute` (`home` |
      `explore` | `activity` | `library` | `profile` | `studio`) or `'search'`
      (rewrites to `explore`)
  - Behavior: build target URL via `WEB_SHELL_ROUTE_PATHS[targetSurface]`,
    append serialized search params + `path` joined; `redirect(target)`
- [x] **Unknown surface** — `params.surface` not in the allowlist → standard
      `not-found.tsx` shell via proxy rewrite before auth.
- [x] **No path segments** — `params.path` empty; target is just
      `WEB_SHELL_ROUTE_PATHS[targetSurface]` (+ search params if any)
- [x] **Path segments present** — `params.path = ['a','b']` →
      `targetParams.set('path', 'a/b')`; appended to target as `?path=a/b`
- [x] **Search params present** — preserved via `toSearchParams` (strings set;
      arrays appended)
- [x] **`?surface=pwa` query** (or any other key) — passed through to the target
      as a query param

Verified 2026-06-30 by `canonical-deep-links.spec.ts` against local web + BFF
dev servers: every V1 shell surface alias lands on its canonical route with
path/query preservation, `search` lands on Explore, `studio` lands in the admin
shell, and unknown aliases render the standard not-found page. The same
contracts are unit-covered in `middleware-public-paths.test.ts`, including the
anonymous-before-auth proxy redirect and raw `Cache-Control: no-store` header.

## Interactions

_None visible._ The route is a one-shot server redirect — no user- facing
interaction. Users land here from external links and are immediately taken to
the canonical shell route.

## Data & contracts

- **Reads**: none (no data fetch)
- **Writes**: none
- **Realtime**: none
- **Caching**: proxy redirect sends `Cache-Control: no-store`; SW does not cache
  redirects per default policy
- **Auth/role check**: the alias itself is anonymous-accessible because proxy
  resolution runs before the auth gate — redirect target is whatever the
  downstream shell route enforces (typically signed-in for `/`, `/explore`,
  `/activity`, `/library`, `/profile`; `/studio` resolves to the admin shell)

### Surface allowlist

From `apps/oshun/web/src/navigation/routes.ts` (`WEB_SHELL_ROUTE_PATHS`):

| surface param | redirect target         |
| ------------- | ----------------------- |
| `home`        | `/`                     |
| `explore`     | `/explore`              |
| `activity`    | `/activity`             |
| `library`     | `/library`              |
| `profile`     | `/profile`              |
| `studio`      | `/studio`               |
| `search`      | `/explore` (rewrite)    |
| anything else | `notFound()` → 404 page |

Note: `isWebShellRoute` is the canonical type guard. `'search'` is rewritten to
`'explore'` as a special-case alias because search lives inside Explore in V1's
IA.

## Cross-references

- Shell: [`shell/02-routing-layouts.md`](../shell/02-routing-layouts.md)
  (catch-all routes section)
- Sibling workspace route: [`workspace-catchall.md`](./workspace-catchall.md)
- Canonical shell targets:
  - [`../customer/02-home-discovery/home.md`](../customer/02-home-discovery/home.md)
    — `home` and `search` (→ explore) targets
  - [`../customer/02-home-discovery/explore.md`](../customer/02-home-discovery/explore.md)
  - [`../customer/02-home-discovery/activity.md`](../customer/02-home-discovery/activity.md)
  - [`../customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
  - [`../customer/09-account/profile.md`](../customer/09-account/profile.md)
- Routes module: `apps/oshun/web/src/navigation/routes.ts`
  (`WEB_SHELL_ROUTE_PATHS`, `isWebShellRoute`)
- Manifest: `apps/oshun/web/public/manifest.json` (`start_url`)
- Feature spec: [`V1/features.md`](../../V1/features.md)

## Open questions / known gaps

- [ ] Document the external systems that link to `/app/<surface>/...` so a
      future routes-module change (renaming or adding a shell surface) updates
      them in lock-step
- [x] Confirm whether unknown aliases yield the same UI as direct `notFound()`
      from another route — verified 2026-06-30; proxy rewrites to `/_not-found`
      and Playwright asserts the standard "Page not found" heading.
- [x] Verify cache headers on the redirect response — verified 2026-06-30; proxy
      alias redirects set `Cache-Control: no-store` so the alias stays
      redirectable if shell routes move.
- [ ] Should `'search'` mapping to `'explore'` add a `?focus=search` or similar
      hint? Today the path goes to `/explore` as-is; the user may expect the
      search input to be focused on landing
