Skip to content

shadowgatevpn/subscription-page

Repository files navigation

Subscription Page

Minimal VPN subscription landing page built with TanStack Start, React, Vite, Tailwind CSS, and shadcn-style UI components.

The page is intended to be served as a branded subscription helper:

  • show current subscription status and traffic from Remnawave;
  • copy, import, and serve the subscription link for supported clients;
  • provide platform-specific download links;
  • keep Remnawave API credentials on the server side only.

Architecture

The frontend renders the UI from src/routes/index.tsx.

For browser requests, subscription card data is loaded through a local server route:

Browser -> GET /api/subscription-info?shortUuid={shortUuid} -> Remnawave GET /api/sub/{shortUuid}/info

The Remnawave API token is read only by src/routes/api/subscription-info.ts. Do not expose it as a VITE_* variable.

For VPN client requests, short subscription URLs are proxied before the React page renders:

VPN client -> GET /{shortUuid} -> Remnawave GET /api/sub/{shortUuid}
VPN client -> GET /{shortUuid}/{clientType} -> Remnawave GET /api/sub/{shortUuid}/{clientType}

The server keeps Remnawave response headers such as subscription-userinfo, profile-title, and routing, so clients like Happ can import the same subscription URL that opens the web page in a browser.

For short subscription URLs such as /<shortUuid>, the shortUuid is read from the route and sent to /api/subscription-info. On /, the server falls back to the last path segment of SUBSCRIPTION_URL, falling back to VITE_SUBSCRIPTION_URL. There is no separate REMNAWAVE_SUBSCRIPTION_SHORT_UUID setting.

Environment

Required server-side variables:

Variable Purpose
REMNAWAVE_PANEL_URL Base URL of the Remnawave panel, for example https://panel.example.com.
REMNAWAVE_API_TOKEN Remnawave API token used by the server route. Never expose this to the browser.

Public UI variables:

Variable Purpose
VITE_SUBSCRIPTION_URL Build-time fallback public subscription URL/template shown, copied, and used for client import buttons on / when runtime SUBSCRIPTION_URL is unset. On /<shortUuid>, the visible route replaces the last path segment for copy/import buttons and for /api/subscription-info?shortUuid=.... If both values are empty, subscription copy/import buttons are hidden on /.
VITE_PAGE_TITLE Header/title shown in the UI. Defaults to Subscription.
VITE_SUPPORT_URL Build-time fallback support link shown as the Telegram/support button if runtime SUPPORT_URL is unset.
VITE_SUBSCRIPTION_NOT_FOUND_REDIRECT_URL Build-time fallback browser redirect URL used when subscription info cannot be loaded and runtime SUBSCRIPTION_NOT_FOUND_REDIRECT_URL is unset. If both values are empty, the page stays open and shows the subscription card with the failed-load state.
VITE_NOT_FOUND_REDIRECT_URL Build-time fallback browser redirect URL used for unmatched 404 routes when runtime NOT_FOUND_REDIRECT_URL is unset. If both values are empty, the built-in 404 page is shown.
VITE_USE_MOCK_SUBSCRIPTION_INFO Optional static/demo mode. Set to true, 1, or yes to show bundled mock subscription data instead of calling /api/subscription-info. Used by the GitHub Pages workflow.

Optional variables:

Variable Purpose
SUPPORT_URL Runtime support link shown as the Telegram/support button. Preferred for Docker/Dokploy because it is read from the running container environment through /api/page-config. If both SUPPORT_URL and VITE_SUPPORT_URL are empty, the support button is hidden.
SUBSCRIPTION_URL Runtime public subscription URL/template shown, copied, and used as the server fallback for /api/subscription-info on /. Preferred over VITE_SUBSCRIPTION_URL.
SUBSCRIPTION_NOT_FOUND_REDIRECT_URL Runtime browser redirect URL used when subscription info cannot be loaded. Preferred over VITE_SUBSCRIPTION_NOT_FOUND_REDIRECT_URL.
NOT_FOUND_REDIRECT_URL Runtime browser redirect URL used for unmatched 404 routes. Preferred over VITE_NOT_FOUND_REDIRECT_URL.
CADDY_AUTH_API_TOKEN Optional X-Api-Key header for protected panel access.
CLOUDFLARE_ZERO_TRUST_CLIENT_ID Optional Cloudflare Access client id.
CLOUDFLARE_ZERO_TRUST_CLIENT_SECRET Optional Cloudflare Access client secret.
EGAMES_COOKIE Optional cookie forwarded to the panel.

Build-Time vs Runtime

Variables prefixed with VITE_ are build-time client configuration. They are read by Vite during npm run build and baked into the browser bundle. Changing them in a running Docker/Dokploy container does not affect the already-built UI; rebuild and redeploy the image after changing a VITE_* value.

Server-side variables without the VITE_ prefix are read by the running server process. In this app, SUPPORT_URL, SUBSCRIPTION_URL, SUBSCRIPTION_NOT_FOUND_REDIRECT_URL, and NOT_FOUND_REDIRECT_URL are runtime overrides: the browser fetches public UI values from /api/page-config, and server routes read the same running process environment. They can be changed in the container environment without rebuilding the image. Their VITE_* counterparts remain build-time fallbacks and are useful for static builds or as image defaults.

VITE_PAGE_TITLE and VITE_USE_MOCK_SUBSCRIPTION_INFO remain build-time settings. To change those in production, set the value before building the image and redeploy it.

Example:

REMNAWAVE_PANEL_URL=https://panel.example.com
REMNAWAVE_API_TOKEN=remnawave_api_token
SUBSCRIPTION_URL=https://vpn.example.com/subscription/user/abc123def456
VITE_PAGE_TITLE=Intezya VPN
SUPPORT_URL=https://t.me/support
SUBSCRIPTION_NOT_FOUND_REDIRECT_URL=https://example.com/support/subscription-not-found
NOT_FOUND_REDIRECT_URL=https://example.com/
VITE_USE_MOCK_SUBSCRIPTION_INFO=false

Subscription Load Failure

On page load, the browser requests /api/subscription-info; on short routes it uses /api/subscription-info?shortUuid=<shortUuid>. If the request fails because the subscription cannot be fetched from Remnawave, the page checks SUBSCRIPTION_NOT_FOUND_REDIRECT_URL, then falls back to VITE_SUBSCRIPTION_NOT_FOUND_REDIRECT_URL.

  • If the variable contains a URL, the browser navigates to that URL.
  • If the variable is empty or unset, the user remains on the page and the subscription card shows the failed-load state.

Route Not Found Redirect

Unmatched browser routes, including / when root rendering is disabled, use the root 404 component. If NOT_FOUND_REDIRECT_URL contains a URL, the browser navigates to that URL. If it is empty or unset, the page falls back to VITE_NOT_FOUND_REDIRECT_URL; if both values are empty, the original 404 page is shown.

GitHub Pages

The repository includes .github/workflows/pages.yml for a static GitHub Pages deployment. The workflow runs on pushes to main and on manual dispatch, builds the app with npm run build, and uploads dist/client through the official GitHub Pages artifact/deploy actions.

TanStack Start normally builds a server-rendered app, so the Pages workflow also runs node scripts/create-pages-index.mjs after the build. That script creates static index.html, 404.html, and .nojekyll files in dist/client around the generated client bundle.

GitHub Pages cannot run the TanStack Start server route /api/subscription-info, so the workflow sets:

VITE_USE_MOCK_SUBSCRIPTION_INFO=true

In that mode the page shows bundled demo subscription data and does not call the server route. Normal non-Pages builds leave this variable unset and continue to load real Remnawave data through /api/subscription-info.

For the repository site https://shadowgatevpn.github.io/subscription-page/, the workflow also builds assets with the /subscription-page/ base path. Before the first deployment, set the repository Pages source to GitHub Actions in GitHub repository settings.

Docker Image Publishing

The repository includes .github/workflows/docker.yml for building and pushing a Docker image to GitHub Container Registry:

ghcr.io/shadowgatevpn/subscription-page

The workflow runs on pushes to main, version tags matching v*, and manual dispatch. It authenticates to ghcr.io with the built-in GITHUB_TOKEN, so no registry secret is required. It publishes tags from the branch/tag name, a sha-<commit> tag, and latest for the default branch.

The image is built from the root Dockerfile. The Docker build sets DOCKER_BUILD=true, which makes vite.config.ts produce a Nitro node-server output under .output. The runtime image serves that output on PORT, defaulting to 3000.

Development

Install dependencies:

npm install

Start the dev server:

npm run dev

Build for production:

npm run build

Preview a production build:

npm run preview

Verification

Run lint:

npm run lint

Run focused assertion tests:

node_modules/.bin/tsx src/lib/i18n.test.ts
node_modules/.bin/tsx src/lib/pages-build.test.ts
node_modules/.bin/tsx src/lib/subscription-info.test.ts
node_modules/.bin/tsx src/lib/not-found-redirect.test.ts
node_modules/.bin/tsx src/lib/subscription-redirect.test.ts
node_modules/.bin/tsx src/lib/subscription-passthrough.test.ts
node_modules/.bin/tsx src/lib/subscription-url.test.ts

Run TypeScript checking:

node_modules/.bin/tsc --noEmit

Important Files

Path Responsibility
Dockerfile Multi-stage Node image for the server-rendered subscription page.
.dockerignore Keeps local build output and dependencies out of Docker build context.
src/routes/index.tsx Main subscription page UI.
src/routes/api/subscription-info.ts Server-only Remnawave subscription info proxy.
src/lib/mock-subscription-info.ts Static demo subscription data used by GitHub Pages builds.
src/lib/pages-build.ts GitHub Pages base path and env-flag helpers.
src/lib/not-found-redirect.ts Decides whether unmatched 404 routes should redirect the browser.
src/lib/subscription-passthrough.ts Proxies non-browser short subscription URL requests to Remnawave.
src/lib/subscription-info.ts Normalizes Remnawave response into card data.
src/lib/subscription-redirect.ts Decides whether a failed subscription load should redirect the browser.
src/lib/subscription-url.ts Extracts shortUuid from the configured subscription URL.
src/lib/i18n.ts UI translations.
src/page-config.ts Public page configuration from VITE_* env values.
scripts/create-pages-index.mjs Creates static Pages entry files around the TanStack Start client bundle.

Security Notes

  • Keep REMNAWAVE_API_TOKEN server-side only.
  • Do not prefix secrets with VITE_; Vite exposes VITE_* variables to browser code.
  • The browser calls only /api/subscription-info; it never calls Remnawave with the API token.
  • The local server route returns normalized card data, not the full Remnawave response.

About

VPN subscription landing page

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors