refactor: delegate webapp build to flake.lib.mkWebapp

Drops the local stdenv.mkDerivation + fetchPnpmDeps in favor of
calling the webapp flake's own lib.mkWebapp (added in
aiolabs/webapp#97). The hub build now runs through the same
builder as the per-standalone builds in server-deploy, which means
ONE pnpmDepsHash lives in webapp's flake.nix and no other repo
carries one.

Options changed:
- ADDED  flake (required): the webapp flake input. Server-deploy
  hosts pin this to inputs.webapp (main) or inputs.webapp-dev
  (staging channel).
- ADDED  brandDir: path to the brand kit. Defaults to the flake's
  committed aiolabs default. Override for per-host branding (logo,
  manifest name, theme colors).
- REMOVED src: replaced by flake (which carries the source).
- REMOVED gitUrl, gitRef: replaced by flake input ref selection.
- REMOVED pnpmDepsHash: managed inside webapp's flake.nix now.
- REMOVED appName: per #99/#100's strict policy, VITE_APP_NAME is
  brand-controlled. Custom names flow through brandDir + brand.json,
  not env vars. Hosts that today set appName must either accept the
  default brand's name or supply a per-host brandDir.

Other options (nostrRelays, lnbitsBaseUrl, hubXUrl, …) flow into
the build via lib.mkWebapp's extraEnv.

nginx root moves from \$out/share/webapp to \$out/dist (lib.mkWebapp's
output layout).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-10 17:04:05 +02:00
commit fe13f3b283

View file

@ -1,134 +1,81 @@
# NixOS module for building and serving the Vue 3 webapp
# NixOS module for serving the Vue 3 webapp.
#
# Build is delegated to the webapp flake's lib.mkWebapp (since
# aiolabs/webapp#97). This module owns:
# - the nginx vhost
# - the per-deployment VITE_* env vars (relays, LNbits URL, hub
# tile URLs, …) that get baked into the bundle
# - brand kit selection via `brandDir`
#
# Per-deployment name customization flows through brand.json under
# `brandDir`, NOT through a `VITE_APP_NAME` knob — see strict policy
# landed in aiolabs/webapp#99 / #100.
{ config, lib, pkgs, ... }:
let
cfg = config.services.webapp;
# Build the Vue 3 webapp using stdenv + pnpm.configHook
webapp = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "aio-webapp";
version = "1.0.0";
webapp = cfg.flake.lib.mkWebapp {
inherit pkgs;
brandDir = cfg.brandDir;
extraEnv = {
ELECTRON_SKIP_BINARY_DOWNLOAD = "1";
NODE_ENV = "production";
# Use src option if provided (flake input), otherwise fetch from git
src = if cfg.src != null then cfg.src else builtins.fetchGit {
url = cfg.gitUrl;
ref = cfg.gitRef;
VITE_NOSTR_RELAYS = builtins.toJSON cfg.nostrRelays;
VITE_ADMIN_PUBKEYS = builtins.toJSON cfg.adminPubkeys;
VITE_LNBITS_BASE_URL = cfg.lnbitsBaseUrl;
VITE_LNBITS_DEBUG = if cfg.lnbitsDebug then "true" else "false";
VITE_LNBITS_NOSTR_TRANSPORT_PUBKEY = cfg.lnbitsNostrTransportPubkey;
VITE_WEBSOCKET_ENABLED = if cfg.websocketEnabled then "true" else "false";
VITE_LIGHTNING_DOMAIN = cfg.lightningDomain;
VITE_VAPID_PUBLIC_KEY = cfg.vapidPublicKey;
VITE_PUSH_NOTIFICATIONS_ENABLED = if cfg.pushNotificationsEnabled then "true" else "false";
VITE_PICTRS_BASE_URL = cfg.pictrsBaseUrl;
VITE_MARKET_NADDR = cfg.marketNaddr;
VITE_DEMO_MODE = if cfg.demoMode then "true" else "false";
VITE_HUB_EVENTS_URL = cfg.hubEventsUrl;
VITE_HUB_LIBRA_URL = cfg.hubLibraUrl;
VITE_HUB_WALLET_URL = cfg.hubWalletUrl;
VITE_HUB_CHAT_URL = cfg.hubChatUrl;
VITE_HUB_FORUM_URL = cfg.hubForumUrl;
VITE_HUB_MARKET_URL = cfg.hubMarketUrl;
VITE_HUB_TASKS_URL = cfg.hubTasksUrl;
VITE_HUB_RESTAURANT_URL = cfg.hubRestaurantUrl;
};
# Fixed-output derivation of the pnpm offline store.
# On first build / lockfile change, set hash = lib.fakeHash and rebuild;
# Nix will report the correct hash to substitute.
pnpmDeps = pkgs.fetchPnpmDeps {
inherit (finalAttrs) pname version src;
hash = cfg.pnpmDepsHash;
fetcherVersion = 3;
};
nativeBuildInputs = [ pkgs.nodejs_20 pkgs.pnpm pkgs.pnpmConfigHook ];
# Skip Electron binary download (we're building a web app, not desktop)
ELECTRON_SKIP_BINARY_DOWNLOAD = "1";
# Environment variables for Vite build (VITE_* are embedded at build time)
# App Configuration
VITE_APP_NAME = cfg.appName;
# Nostr Configuration
VITE_NOSTR_RELAYS = builtins.toJSON cfg.nostrRelays;
VITE_ADMIN_PUBKEYS = builtins.toJSON cfg.adminPubkeys;
# API Configuration
VITE_LNBITS_BASE_URL = cfg.lnbitsBaseUrl;
VITE_LNBITS_DEBUG = if cfg.lnbitsDebug then "true" else "false";
VITE_WEBSOCKET_ENABLED = if cfg.websocketEnabled then "true" else "false";
VITE_LNBITS_NOSTR_TRANSPORT_PUBKEY = cfg.lnbitsNostrTransportPubkey;
# Lightning Address Domain
VITE_LIGHTNING_DOMAIN = cfg.lightningDomain;
# Push Notifications
VITE_VAPID_PUBLIC_KEY = cfg.vapidPublicKey;
VITE_PUSH_NOTIFICATIONS_ENABLED = if cfg.pushNotificationsEnabled then "true" else "false";
# Image Upload Configuration (pict-rs)
VITE_PICTRS_BASE_URL = cfg.pictrsBaseUrl;
# Market Configuration
VITE_MARKET_NADDR = cfg.marketNaddr;
# Demo Mode
VITE_DEMO_MODE = if cfg.demoMode then "true" else "false";
# Hub → standalone navigation URLs.
# The chakra tiles in src/pages/Hub.vue build <a href> from these. With
# path-mode deployment, set these to canonical trailing-slash URLs:
# hubMarketUrl = "https://demo.example.com/market/";
# With subdomain deployment:
# hubMarketUrl = "https://market.example.com";
# When unset (empty string), the corresponding chakra renders ghosted
# (non-link) — useful for hosts that don't deploy a given module.
VITE_HUB_EVENTS_URL = cfg.hubEventsUrl;
VITE_HUB_LIBRA_URL = cfg.hubLibraUrl;
VITE_HUB_WALLET_URL = cfg.hubWalletUrl;
VITE_HUB_CHAT_URL = cfg.hubChatUrl;
VITE_HUB_FORUM_URL = cfg.hubForumUrl;
VITE_HUB_MARKET_URL = cfg.hubMarketUrl;
VITE_HUB_TASKS_URL = cfg.hubTasksUrl;
VITE_HUB_RESTAURANT_URL = cfg.hubRestaurantUrl;
# Additional env vars
NODE_ENV = "production";
# pnpm.configHook sets up the offline store and runs
# `pnpm install --offline --frozen-lockfile` before buildPhase.
buildPhase = ''
runHook preBuild
pnpm run build
runHook postBuild
'';
# Install the built static files (Vite outputs to ./dist)
installPhase = ''
runHook preInstall
mkdir -p $out/share/webapp
cp -r dist/* $out/share/webapp/
runHook postInstall
'';
# Don't run pnpm test
doCheck = false;
meta = with lib; {
description = "AIO Community Hub - Vue 3 webapp";
license = licenses.mit;
platforms = platforms.linux;
};
});
};
in {
options.services.webapp = {
enable = lib.mkEnableOption "AIO webapp";
src = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
flake = lib.mkOption {
type = lib.types.attrs;
example = lib.literalExpression "inputs.webapp";
description = ''
Source tree for the webapp. When set, this is used directly instead of
fetching via builtins.fetchGit. Use this with flake inputs for pure evaluation.
The webapp flake (as a flake input, not raw source). The module
calls `flake.lib.mkWebapp` to build. Pin per-host to pick the
right channel `inputs.webapp` for main, `inputs.webapp-dev`
for the dev/staging channel.
'';
};
gitUrl = lib.mkOption {
type = lib.types.str;
default = "https://git.atitlan.io/aiolabs/webapp";
description = "Git repository URL for the webapp source";
};
gitRef = lib.mkOption {
type = lib.types.str;
default = "main";
example = "demo";
description = "Git branch or tag to build from";
brandDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.flake}/branding/default";
defaultText = lib.literalExpression ''"\${cfg.flake}/branding/default"'';
description = ''
Path to the brand kit directory consumed by the build. Defaults
to the webapp flake's committed aiolabs brand. Override to a
per-host directory in server-deploy (e.g.
`./../branding/cfaun`) for per-host logo + manifest name +
theme colors. See `branding/README.md` in the webapp repo.
'';
};
domain = lib.mkOption {
@ -137,23 +84,6 @@ in {
description = "Domain name for the webapp";
};
pnpmDepsHash = lib.mkOption {
type = lib.types.str;
default = lib.fakeHash;
description = ''
SRI hash of pnpm dependencies fetched from `pnpm-lock.yaml`.
Initially set to `lib.fakeHash`; on first build with a new lockfile,
Nix will fail with the correct hash to substitute in.
'';
};
# App Configuration
appName = lib.mkOption {
type = lib.types.str;
default = "MyApp";
description = "Application name displayed in the UI";
};
# Nostr Configuration
nostrRelays = lib.mkOption {
type = lib.types.listOf lib.types.str;
@ -308,24 +238,21 @@ in {
};
config = lib.mkIf cfg.enable {
# Nginx virtualHost for webapp (global nginx settings are in config/nginx.nix)
# lib.mkWebapp's output layout is `$out/dist/`, not `$out/share/webapp/`,
# so the vhost root points directly at dist/.
services.nginx.virtualHosts.${cfg.domain} = {
# SSL configuration
forceSSL = cfg.enableSSL;
enableACME = cfg.enableSSL;
# Serve the built webapp
root = "${webapp}/share/webapp";
root = "${webapp}/dist";
locations = {
"/" = {
# Try files, fallback to index.html for SPA routing.
# Everything matching this prefix ultimately serves the SPA
# shell (index.html), which must revalidate on every load so
# new deploys are picked up without the browser holding a
# stale shell pointing at deleted hashed assets. The more-
# specific regex location below overrides this for hashed
# static assets, which stay immutable.
# SPA shell must revalidate on every load so new deploys are
# picked up without the browser holding a stale shell pointing
# at deleted hashed assets. The more-specific regex location
# below overrides this for hashed static assets, which stay
# immutable.
tryFiles = "$uri $uri/ /index.html";
extraConfig = ''
add_header Cache-Control "no-cache";