welplan2 provides TypeScript clients and a web app for browsing cafeteria menus from Samsung Welstory Plus and Shinsegae Food PlanEAT Choice.
The repository contains a SvelteKit web app, shared TypeScript models, and vendor-specific clients used to fetch menu, nutrition, and restaurant data.
- Aggregates restaurant and meal-time data from Welstory Plus and PlanEAT Choice.
- Supports gallery, take-in, and take-out menu views.
- Displays calories and detailed nutrition in the web UI.
- Caches restaurants, meal times, menus, and menu details in PostgreSQL.
- Uses a dedicated worker for periodic cache prefetching. The web app reads from PostgreSQL cache only.
- Builds and publishes a Docker image to GHCR.
- Publishes client packages to the GitHub Packages npm registry.
| Path | Package | Purpose |
|---|---|---|
model |
@pmh-only/welplan2-model |
Shared cafeteria domain types used across the repository. |
impl/welstory_plus |
@pmh-only/welplan2-welstory-plus |
Welstory Plus API client used by the web app. |
impl/planeat_choice |
@pmh-only/welplan2-planeat-choice |
PlanEAT Choice API client used by the web app. |
webapp |
@pmh-only/welplan2-webapp |
SvelteKit application for browsing menus. |
worker |
@pmh-only/welplan2-worker |
Background poller that prefetches menus into PostgreSQL. |
- Node.js 22 or newer recommended.
pnpmvia Corepack.- Welstory credentials if you want Welstory data.
The app now loads variables from .env automatically (same directory as the running package).
You can also set DOTENV_PATH to point at a custom env file.
| Variable | Required | Default | Description |
|---|---|---|---|
WELSTORY_USERNAME |
For Welstory | none | Welstory Plus login ID. |
WELSTORY_PASSWORD |
For Welstory | none | Welstory Plus password. |
WELSTORY_DEVICE_ID |
No | generated UUID | Optional device identifier sent to Welstory. |
WELPLAN_VERBOSE_LOGS |
No | off | Enables all verbose server, sync, traffic, and auth logs. |
WELPLAN_SYNC_LOGS |
No | off | Enables detailed cache warmup, cache hit/miss, and poller logs. |
WELPLAN_TRAFFIC_LOGS |
No | off | Enables inbound web request logs and outbound vendor API traffic logs. |
WELPLAN_AUTH_LOGS |
No | off | Enables detailed Welstory login and session refresh logs. |
DOTENV_PATH |
No | auto | Custom path to a dotenv file (for worker or custom layouts). |
DATABASE_URL |
No | generated | PostgreSQL connection URL used by Drizzle. |
PGHOST |
No | localhost |
PostgreSQL host (used when DATABASE_URL is not set). |
PGPORT |
No | 5432 |
PostgreSQL port (used when DATABASE_URL is not set). |
PGDATABASE |
No | welplan2 |
PostgreSQL database name (used when DATABASE_URL is not set). |
PGUSER |
No | welplan2 |
PostgreSQL user (used when DATABASE_URL is not set). |
PGPASSWORD |
No | none | PostgreSQL password (used when DATABASE_URL is not set). |
HOST |
No | 0.0.0.0 |
Host used by the production Node server. |
PORT |
No | 3000 |
Port used by the production Node server. |
ORIGIN |
Recommended behind proxy | request origin | Public origin, e.g. https://welplan.pmh.codes, used by SvelteKit CSRF checks and absolute URLs. |
HOST_HEADER |
No | none | Header adapter-node should trust for host, e.g. x-forwarded-host, when using a reverse proxy. |
PROTOCOL_HEADER |
No | none | Header adapter-node should trust for protocol, e.g. x-forwarded-proto, when using a reverse proxy. |
ADMIN_OIDC_RESPONSE_MODE |
No | query |
OIDC response mode. Keep query to avoid cross-site POST callbacks being blocked by SvelteKit CSRF checks. |
TWA_PACKAGE_NAME |
For Android TWA | none | Android package name allowed to claim this web origin through Digital Asset Links. |
TWA_SHA256_CERT_FINGERPRINTS |
For Android TWA | none | Comma or whitespace separated SHA-256 signing certificate fingerprints served from /.well-known/assetlinks.json. |
| Variable | Required | Default | Description |
|---|---|---|---|
WORKER_ACTIVE_PREFETCH_INTERVAL_MS |
No | 600000 |
Poll interval for active user-selected restaurants. |
WORKER_FULL_SCAN_INTERVAL_MS |
No | 21600000 |
Poll interval for full cache scan across all cached restaurants. |
WORKER_ACTIVE_PREFETCH_DAYS |
No | 2 |
Days from today for active prefetch. |
PlanEAT Choice requests do not currently require credentials.
All log flags accept common truthy values such as 1, true, yes, on, debug, or verbose.
Install dependencies:
corepack enable
pnpm installBuild the TypeScript libraries:
pnpm buildStart the web app in development mode:
pnpm --filter @pmh-only/welplan2-webapp devStart only the worker in development mode:
pnpm --filter @pmh-only/welplan2-worker devCreate user, database, and privileges:
sudo -u postgres psql -c "CREATE DATABASE welplan2;"
sudo -u postgres psql -c "CREATE USER welplan2_user WITH PASSWORD 'replace-me';"
sudo -u postgres psql -d welplan2 -c "GRANT ALL PRIVILEGES ON DATABASE welplan2 TO welplan2_user;"Then run app services with matching credentials:
export DATABASE_URL="postgresql://welplan2_user:replace-me@localhost:5432/welplan2"Run the worker process separately from web app:
pnpm --filter @pmh-only/welplan2-worker startBuild the production web app bundle:
pnpm --filter @pmh-only/welplan2-webapp buildRun the production server locally:
pnpm --filter @pmh-only/welplan2-webapp start/or/gallery: gallery-style menu view./takein: redirect to the current take-in menu./takeout: redirect to the current take-out menu./restaurants: manage the restaurant list stored in thewelplan_restaurantscookie./api/cache/status: inspect cache counts./api/cache/clear: clear cached data./.well-known/assetlinks.json: Android Digital Asset Links for Trusted Web Activity verification when TWA env vars are configured.
To install the published packages, configure npm for the @pmh-only scope:
@pmh-only:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKENnpm install @pmh-only/welplan2-welstory-plusimport { WelstoryPlusClient } from '@pmh-only/welplan2-welstory-plus'
const client = new WelstoryPlusClient({
username: process.env.WELSTORY_USERNAME,
password: process.env.WELSTORY_PASSWORD
})
const restaurants = await client.getRestaurants()
const mealTimes = await client.getMealTimes(restaurants[0])
const menus = await client.getMenus(restaurants[0], '20260416', mealTimes[0].id)npm install @pmh-only/welplan2-planeat-choiceimport { PlaneatChoiceClient } from '@pmh-only/welplan2-planeat-choice'
const client = new PlaneatChoiceClient()
const restaurants = await client.getRestaurants()
const mealTimes = await client.getMealTimes(restaurants[0])
const menus = await client.getMenus(restaurants[0], '20260416', mealTimes[0].id)Shared types for restaurants, meal times, menus, nutrition, and the CafeteriaClient contract.
import type { CafeteriaClient, MealTime, Menu, Restaurant } from '@pmh-only/welplan2-model'Build the image:
docker build -t welplan2 .Run it:
docker run --rm \
-p 3000:3000 \
-e WELSTORY_USERNAME=your-id \
-e WELSTORY_PASSWORD=your-password \
-e DATABASE_URL=postgresql://user:password@postgres:5432/welplan2 \
welplan2Webapp is cache-only in normal operation, so web-only containers should be paired with the worker for cache warm-up.
The container uses PostgreSQL for cache storage and does not create a local /data volume.
Trusted Web Activity is Android-only and is published through Google Play. It is not an Apple App Store format; for iOS, use the installed PWA experience or build a native iOS shell that satisfies App Store Review requirements.
The web app already provides the core PWA pieces needed by a TWA: HTTPS-compatible SvelteKit app, generated manifest.webmanifest, icons, screenshots, and service worker registration. The remaining production requirement is proving ownership between the Android app and this web origin with Digital Asset Links.
Configure the production web app with the Android package name and SHA-256 signing certificate fingerprint:
TWA_PACKAGE_NAME=com.example.welplan
TWA_SHA256_CERT_FINGERPRINTS=AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99Use the Google Play app signing certificate fingerprint for production releases. If you test locally with a debug or upload key, include that certificate fingerprint too, separated by a comma.
Verify the web-side association after deployment:
curl https://welplan.example.com/.well-known/assetlinks.jsonGenerate the Android wrapper with Bubblewrap:
pnpm dlx @bubblewrap/cli init --manifest=https://welplan.example.com/manifest.webmanifestDuring Bubblewrap setup, use the same package name as TWA_PACKAGE_NAME, set the start URL to /, and use the production HTTPS origin. Build a signed Android App Bundle from the generated Android project and upload the .aab to Google Play Console.
Google Play release checklist:
- Production site is available on HTTPS.
/.well-known/assetlinks.jsonreturns the package name and Play app signing SHA-256 fingerprint.manifest.webmanifesthasname,short_name,start_url,scope,display, theme colors, and 192/512 icons.- Service worker is available at
/sw.jsafter a production build. - Store listing, screenshots, privacy policy, data safety, and content rating are completed in Play Console.
.github/workflows/docker-webapp.yml builds amd64 and arm64 images and publishes them to:
ghcr.io/pmh-only/welplan2:sha-<commit>ghcr.io/pmh-only/welplan2:lateston the default branch
Published packages are stored in the GitHub Packages npm registry under the @pmh-only scope.
.github/workflows/release-packages.yml publishes these packages to GitHub Packages:
@pmh-only/welplan2-model@pmh-only/welplan2-welstory-plus@pmh-only/welplan2-planeat-choice
Automatic release flow:
- Push changes to
main, or run the workflow manually. - The workflow generates a unique package version for that run.
- It publishes
@pmh-only/welplan2-modelfirst. - It then publishes
@pmh-only/welplan2-welstory-plusand@pmh-only/welplan2-planeat-choicewith the matching model dependency version. - All three packages are published with the
latestdist-tag, so installs without an explicit version resolve to the newest published build.
No manual version bump or git tag is required.
Release note:
GitHub Packages still requires immutable package versions. This workflow hides that manual step by generating a unique internal version on every release while keeping the consumer-facing install target on latest.
GitHub Packages note:
The workflow publishes with the repository GITHUB_TOKEN. For installs outside Actions, use a GitHub token with package read access. After the first publish, verify package visibility in GitHub Packages if you want public consumption.
Useful repository commands:
pnpm build
pnpm lint
pnpm --filter @pmh-only/welplan2-webapp buildThe repository currently does not define an automated test suite.