contrib,nix: local, sandboxed atmosphere infra #853

open
opened by boltless.me targeting master from sl/kzmmroxoztll

resubmitting because original PR (#684) become quite a mess.

It is pretty rough, and I'd like to know if other people can run this fine.

contrib,nix: local, sandboxed atmosphere infra

Add sandboxed atmosphere environment for local testing. This new vm
contains everything required to run local test appview including PLC,
PDS, Jetstream (listening to single PDS), knot and spindle.

I'm using my custom `tngl.boltless.dev` domain which resolves to
`127.0.0.1` without any proxy.

PLC: plc.tngl.boltless.dev
PDS: pds.tngl.boltless.dev
Jetstream: jetstream.tngl.boltless.dev
Knot: knot.tngl.boltless.dev
Spindle: spindle.tngl.boltless.dev

TLS is supported with caddy service running inside the vm.

note: `pds.env` file here is hard copy to be used for contrib/scripts.
note: upgraded pds package in order to set email settings

Signed-off-by: Seongmin Lee <git@boltless.me>
+31
contrib/example.env
···
···
+
# NOTE: put actual DIDs here
+
alice_did=did:plc:alice-did
+
tangled_did=did:plc:tangled-did
+
+
#core
+
export TANGLED_DEV=true
+
export TANGLED_APPVIEW_HOST=http://127.0.0.1:3000
+
# plc
+
export TANGLED_PLC_URL=https://plc.tngl.boltless.dev
+
# jetstream
+
export TANGLED_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe
+
# label
+
export TANGLED_LABEL_GFI=at://${tangled_did}/sh.tangled.label.definition/good-first-issue
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_GFI
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/assignee
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/documentation
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/duplicate
+
export TANGLED_LABEL_DEFAULTS=$TANGLED_LABEL_DEFAULTS,at://${tangled_did}/sh.tangled.label.definition/wontfix
+
+
# vm settings
+
export TANGLED_VM_PLC_URL=https://plc.tngl.boltless.dev
+
export TANGLED_VM_JETSTREAM_ENDPOINT=wss://jetstream.tngl.boltless.dev/subscribe
+
export TANGLED_VM_KNOT_HOST=knot.tngl.boltless.dev
+
export TANGLED_VM_KNOT_OWNER=$alice_did
+
export TANGLED_VM_SPINDLE_HOST=spindle.tngl.boltless.dev
+
export TANGLED_VM_SPINDLE_OWNER=$alice_did
+
+
if [ -n "${TANGLED_RESEND_API_KEY:-}" ] && [ -n "${TANGLED_RESEND_SENT_FROM:-}" ]; then
+
export TANGLED_VM_PDS_EMAIL_SMTP_URL=smtps://resend:$TANGLED_RESEND_API_KEY@smtp.resend.com:465/
+
export TANGLED_VM_PDS_EMAIL_FROM_ADDRESS=$TANGLED_RESEND_SENT_FROM
+
fi
+12
contrib/pds.env
···
···
+
LOG_ENABLED=true
+
+
PDS_JWT_SECRET=8cae8bffcc73d9932819650791e4e89a
+
PDS_ADMIN_PASSWORD=d6a902588cd93bee1af83f924f60cfd3
+
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7
+
+
PDS_DATA_DIRECTORY=/pds
+
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
+
+
PDS_DID_PLC_URL=http://localhost:8080
+
PDS_HOSTNAME=pds.tngl.boltless.dev
+
PDS_PORT=3000
+26
contrib/readme.md
···
···
+
# how to setup local appview dev environment
+
+
Appview requires several microservices from knot and spindle to entire atproto infra. This test environment is implemented under nixos vm.
+
+
1. copy `contrib/example.env` to `.env`, fill it and source it
+
2. run vm
+
```bash
+
nix run --impure .#vm
+
```
+
3. trust the generated cert from host machine
+
```bash
+
# for macos
+
sudo security add-trusted-cert -d -r trustRoot \
+
-k /Library/Keychains/System.keychain \
+
./nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/root.crt
+
```
+
4. restart vm (now nixos self-trust caddy's rootCA)
+
5. create test accounts with valid emails (use [`create-test-account.sh`](./scripts/create-test-account.sh))
+
6. create default labels (use [`setup-const-records`](./scripts/setup-const-records.sh))
+
7. restart vm with correct owner-did
+
+
for git-https, you should change your local git config:
+
```
+
[http "https://knot.tngl.boltless.dev"]
+
sslCAPath = /Users/boltless/repo/tangled/nix/vm-data/caddy/.local/share/caddy/pki/authorities/local/
+
```
+68
contrib/scripts/create-test-account.sh
···
···
+
#!/bin/bash
+
set -o errexit
+
set -o nounset
+
set -o pipefail
+
+
source "$(dirname "$0")/../pds.env"
+
+
# PDS_HOSTNAME=
+
# PDS_ADMIN_PASSWORD=
+
+
# curl a URL and fail if the request fails.
+
function curl_cmd_get {
+
curl --fail --silent --show-error "$@"
+
}
+
+
# curl a URL and fail if the request fails.
+
function curl_cmd_post {
+
curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@"
+
}
+
+
# curl a URL but do not fail if the request fails.
+
function curl_cmd_post_nofail {
+
curl --silent --show-error --request POST --header "Content-Type: application/json" "$@"
+
}
+
+
USERNAME="${1:-}"
+
+
if [[ "${USERNAME}" == "" ]]; then
+
read -p "Enter a username: " USERNAME
+
fi
+
+
if [[ "${USERNAME}" == "" ]]; then
+
echo "ERROR: missing USERNAME parameter." >/dev/stderr
+
echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr
+
exit 1
+
fi
+
+
EMAIL=${USERNAME}@${PDS_HOSTNAME}
+
+
PASSWORD="password"
+
INVITE_CODE="$(curl_cmd_post \
+
--user "admin:${PDS_ADMIN_PASSWORD}" \
+
--data '{"useCount": 1}' \
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code'
+
)"
+
RESULT="$(curl_cmd_post_nofail \
+
--data "{\"email\":\"${EMAIL}\", \"handle\":\"${USERNAME}.${PDS_HOSTNAME}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount"
+
)"
+
+
DID="$(echo $RESULT | jq --raw-output '.did')"
+
if [[ "${DID}" != did:* ]]; then
+
ERR="$(echo ${RESULT} | jq --raw-output '.message')"
+
echo "ERROR: ${ERR}" >/dev/stderr
+
echo "Usage: $0 <EMAIL> <HANDLE>" >/dev/stderr
+
exit 1
+
fi
+
+
echo
+
echo "Account created successfully!"
+
echo "-----------------------------"
+
echo "Handle : ${USERNAME}.${PDS_HOSTNAME}"
+
echo "DID : ${DID}"
+
echo "Password : ${PASSWORD}"
+
echo "-----------------------------"
+
echo "This is a test account with an insecure password."
+
echo "Make sure it's only used for development."
+
echo
+106
contrib/scripts/setup-const-records.sh
···
···
+
#!/bin/bash
+
set -o errexit
+
set -o nounset
+
set -o pipefail
+
+
source "$(dirname "$0")/../pds.env"
+
+
# PDS_HOSTNAME=
+
+
# curl a URL and fail if the request fails.
+
function curl_cmd_get {
+
curl --fail --silent --show-error "$@"
+
}
+
+
# curl a URL and fail if the request fails.
+
function curl_cmd_post {
+
curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@"
+
}
+
+
# curl a URL but do not fail if the request fails.
+
function curl_cmd_post_nofail {
+
curl --silent --show-error --request POST --header "Content-Type: application/json" "$@"
+
}
+
+
USERNAME="${1:-}"
+
+
if [[ "${USERNAME}" == "" ]]; then
+
read -p "Enter a username: " USERNAME
+
fi
+
+
if [[ "${USERNAME}" == "" ]]; then
+
echo "ERROR: missing USERNAME parameter." >/dev/stderr
+
echo "Usage: $0 ${SUBCOMMAND} <USERNAME>" >/dev/stderr
+
exit 1
+
fi
+
+
SESS_RESULT="$(curl_cmd_post \
+
--data "$(cat <<EOF
+
{
+
"identifier": "$USERNAME",
+
"password": "password"
+
}
+
EOF
+
)" \
+
https://pds.tngl.boltless.dev/xrpc/com.atproto.server.createSession
+
)"
+
+
echo $SESS_RESULT | jq
+
+
DID="$(echo $SESS_RESULT | jq --raw-output '.did')"
+
ACCESS_JWT="$(echo $SESS_RESULT | jq --raw-output '.accessJwt')"
+
+
function add_label_def {
+
local color=$1
+
local name=$2
+
echo $color
+
echo $name
+
local json_payload=$(cat <<EOF
+
{
+
"repo": "$DID",
+
"collection": "sh.tangled.label.definition",
+
"rkey": "$name",
+
"record": {
+
"name": "$name",
+
"color": "$color",
+
"scope": ["sh.tangled.repo.issue"],
+
"multiple": false,
+
"createdAt": "2025-09-22T11:14:35+01:00",
+
"valueType": {"type": "null", "format": "any"}
+
}
+
}
+
EOF
+
)
+
echo $json_payload
+
echo $json_payload | jq
+
RESULT="$(curl_cmd_post \
+
--data "$json_payload" \
+
-H "Authorization: Bearer ${ACCESS_JWT}" \
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord")"
+
echo $RESULT | jq
+
}
+
+
add_label_def '#64748b' 'wontfix'
+
add_label_def '#8B5CF6' 'good-first-issue'
+
add_label_def '#ef4444' 'duplicate'
+
add_label_def '#06b6d4' 'documentation'
+
json_payload=$(cat <<EOF
+
{
+
"repo": "$DID",
+
"collection": "sh.tangled.label.definition",
+
"rkey": "assignee",
+
"record": {
+
"name": "assignee",
+
"color": "#10B981",
+
"scope": ["sh.tangled.repo.issue", "sh.tangled.repo.pull"],
+
"multiple": false,
+
"createdAt": "2025-09-22T11:14:35+01:00",
+
"valueType": {"type": "string", "format": "did"}
+
}
+
}
+
EOF
+
)
+
curl_cmd_post \
+
--data "$json_payload" \
+
-H "Authorization: Bearer ${ACCESS_JWT}" \
+
"https://${PDS_HOSTNAME}/xrpc/com.atproto.repo.createRecord"
+21 -3
flake.nix
···
spindle = self.callPackage ./nix/pkgs/spindle.nix {};
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
knot = self.callPackage ./nix/pkgs/knot.nix {};
});
in {
overlays.default = final: prev: {
-
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview;
};
packages = forAllSystems (system: let
···
staticPackages = mkPackageSet pkgs.pkgsStatic;
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
in {
-
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib;
pkgsStatic-appview = staticPackages.appview;
pkgsStatic-knot = staticPackages.knot;
···
rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1)
cd "$rootDir"
-
mkdir -p nix/vm-data/{knot,repos,spindle,spindle-logs}
export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data"
exec ${pkgs.lib.getExe
···
services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle;
};
};
}
···
spindle = self.callPackage ./nix/pkgs/spindle.nix {};
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
knot = self.callPackage ./nix/pkgs/knot.nix {};
+
plc = self.callPackage ./nix/pkgs/did-method-plc.nix {};
+
jetstream = self.callPackage ./nix/pkgs/bluesky-jetstream.nix {};
});
in {
overlays.default = final: prev: {
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview plc jetstream;
};
packages = forAllSystems (system: let
···
staticPackages = mkPackageSet pkgs.pkgsStatic;
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
in {
+
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib plc jetstream;
pkgsStatic-appview = staticPackages.appview;
pkgsStatic-knot = staticPackages.knot;
···
rootDir=$(jj --ignore-working-copy root || git rev-parse --show-toplevel) || (echo "error: can't find repo root?"; exit 1)
cd "$rootDir"
+
mkdir -p nix/vm-data/{caddy,knot,repos,spindle,spindle-logs}
export TANGLED_VM_DATA_DIR="$rootDir/nix/vm-data"
exec ${pkgs.lib.getExe
···
services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle;
};
+
nixosModules.jetstream = {
+
lib,
+
pkgs,
+
...
+
}: {
+
imports = [./nix/modules/jetstream.nix];
+
services.jetstream.package = lib.mkDefault self.packages.${pkgs.system}.jetstream;
+
};
+
nixosModules.did-method-plc = {
+
lib,
+
pkgs,
+
...
+
}: {
+
imports = [./nix/modules/did-method-plc.nix];
+
services.plc.package = lib.mkDefault self.packages.${pkgs.system}.plc;
+
};
};
}
+76
nix/modules/did-method-plc.nix
···
···
+
{
+
config,
+
pkgs,
+
lib,
+
...
+
}: let
+
cfg = config.services.plc;
+
in
+
with lib; {
+
options.services.plc = {
+
enable = mkEnableOption "did-method-plc server";
+
package = mkPackageOption pkgs "plc" {};
+
};
+
config = mkIf cfg.enable {
+
services.postgresql = {
+
enable = true;
+
package = pkgs.postgresql_14;
+
ensureDatabases = ["plc"];
+
ensureUsers = [
+
{
+
name = "pg";
+
# ensurePermissions."DATABASE plc" = "ALL PRIVILEGES";
+
}
+
];
+
authentication = ''
+
local all all trust
+
host all all 127.0.0.1/32 trust
+
'';
+
};
+
systemd.services.plc = {
+
description = "did-method-plc";
+
+
after = ["postgresql.service"];
+
wants = ["postgresql.service"];
+
wantedBy = ["multi-user.target"];
+
+
environment = let
+
db_creds_json = builtins.toJSON {
+
username = "pg";
+
password = "";
+
host = "127.0.0.1";
+
port = 5432;
+
};
+
in {
+
# TODO: inherit from config
+
DEBUG_MODE = "1";
+
LOG_ENABLED = "true";
+
LOG_LEVEL = "debug";
+
LOG_DESTINATION = "1";
+
ENABLE_MIGRATIONS = "true";
+
DB_CREDS_JSON = db_creds_json;
+
DB_MIGRATE_CREDS_JSON = db_creds_json;
+
PLC_VERSION = "0.0.1";
+
PORT = "8080";
+
};
+
+
serviceConfig = {
+
ExecStart = getExe cfg.package;
+
User = "plc";
+
Group = "plc";
+
StateDirectory = "plc";
+
StateDirectoryMode = "0755";
+
Restart = "always";
+
+
# Hardening
+
};
+
};
+
users = {
+
users.plc = {
+
group = "plc";
+
isSystemUser = true;
+
};
+
groups.plc = {};
+
};
+
};
+
}
+64
nix/modules/jetstream.nix
···
···
+
{
+
config,
+
pkgs,
+
lib,
+
...
+
}: let
+
cfg = config.services.jetstream;
+
in
+
with lib; {
+
options.services.jetstream = {
+
enable = mkEnableOption "jetstream server";
+
package = mkPackageOption pkgs "jetstream" {};
+
+
# dataDir = mkOption {
+
# type = types.str;
+
# default = "/var/lib/jetstream";
+
# description = "directory to store data (pebbleDB)";
+
# };
+
livenessTtl = mkOption {
+
type = types.int;
+
default = 15;
+
description = "time to restart when no event detected (seconds)";
+
};
+
websocketUrl = mkOption {
+
type = types.str;
+
default = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos";
+
description = "full websocket path to the ATProto SubscribeRepos XRPC endpoint";
+
};
+
};
+
config = mkIf cfg.enable {
+
systemd.services.jetstream = {
+
description = "jetstream";
+
after = ["network.target" "pds.service"];
+
wantedBy = ["multi-user.target"];
+
+
serviceConfig = {
+
User = "jetstream";
+
Group = "jetstream";
+
StateDirectory = "jetstream";
+
StateDirectoryMode = "0755";
+
# preStart = ''
+
# mkdir -p "${cfg.dataDir}"
+
# chown -R jetstream:jetstream "${cfg.dataDir}"
+
# '';
+
# WorkingDirectory = cfg.dataDir;
+
Environment = [
+
"JETSTREAM_DATA_DIR=/var/lib/jetstream/data"
+
"JETSTREAM_LIVENESS_TTL=${toString cfg.livenessTtl}s"
+
"JETSTREAM_WS_URL=${cfg.websocketUrl}"
+
];
+
ExecStart = getExe cfg.package;
+
Restart = "always";
+
RestartSec = 5;
+
};
+
};
+
users = {
+
users.jetstream = {
+
group = "jetstream";
+
isSystemUser = true;
+
};
+
groups.jetstream = {};
+
};
+
};
+
}
+20
nix/pkgs/bluesky-jetstream.nix
···
···
+
{
+
buildGoModule,
+
fetchFromGitHub,
+
}:
+
buildGoModule {
+
pname = "jetstream";
+
version = "0.1.0";
+
src = fetchFromGitHub {
+
owner = "bluesky-social";
+
repo = "jetstream";
+
rev = "7d7efa58d7f14101a80ccc4f1085953948b7d5de";
+
sha256 = "sha256-1e9SL/8gaDPMA4YZed51ffzgpkptbMd0VTbTTDbPTFw=";
+
};
+
subPackages = ["cmd/jetstream"];
+
vendorHash = "sha256-/21XJQH6fo9uPzlABUAbdBwt1O90odmppH6gXu2wkiQ=";
+
doCheck = false;
+
meta = {
+
mainProgram = "jetstream";
+
};
+
}
+65
nix/pkgs/did-method-plc.nix
···
···
+
# inspired by https://github.com/NixOS/nixpkgs/blob/333bfb7c258fab089a834555ea1c435674c459b4/pkgs/by-name/ga/gatsby-cli/package.nix
+
{
+
lib,
+
stdenv,
+
fetchFromGitHub,
+
fetchYarnDeps,
+
yarnConfigHook,
+
yarnBuildHook,
+
nodejs,
+
makeBinaryWrapper,
+
}:
+
stdenv.mkDerivation (finalAttrs: {
+
pname = "plc";
+
version = "0.0.1";
+
+
src = fetchFromGitHub {
+
owner = "did-method-plc";
+
repo = "did-method-plc";
+
rev = "158ba5535ac3da4fd4309954bde41deab0b45972";
+
sha256 = "sha256-O5smubbrnTDMCvL6iRyMXkddr5G7YHxkQRVMRULHanQ=";
+
};
+
postPatch = ''
+
# remove dd-trace dependency
+
sed -i '3d' packages/server/service/index.js
+
'';
+
+
yarnOfflineCache = fetchYarnDeps {
+
yarnLock = finalAttrs.src + "/yarn.lock";
+
hash = "sha256-g8GzaAbWSnWwbQjJMV2DL5/ZlWCCX0sRkjjvX3tqU4Y=";
+
};
+
+
nativeBuildInputs = [
+
yarnConfigHook
+
yarnBuildHook
+
nodejs
+
makeBinaryWrapper
+
];
+
yarnBuildScript = "lerna";
+
yarnBuildFlags = [
+
"run"
+
"build"
+
"--scope"
+
"@did-plc/server"
+
"--include-dependencies"
+
];
+
+
installPhase = ''
+
runHook preInstall
+
+
mkdir -p $out/lib/node_modules/
+
mv packages/ $out/lib/packages/
+
mv node_modules/* $out/lib/node_modules/
+
+
makeWrapper ${lib.getExe nodejs} $out/bin/plc \
+
--add-flags $out/lib/packages/server/service/index.js \
+
--add-flags --enable-source-maps \
+
--set NODE_PATH $out/lib/node_modules
+
+
runHook postInstall
+
'';
+
+
meta = {
+
mainProgram = "plc";
+
};
+
})
+107
nix/vm.nix
···
nixpkgs.lib.nixosSystem {
inherit system;
modules = [
self.nixosModules.knot
self.nixosModules.spindle
({
···
diskSize = 10 * 1024;
cores = 2;
forwardPorts = [
# ssh
{
from = "host";
···
# as SQLite is incompatible with them. So instead we
# mount the shared directories to a different location
# and copy the contents around on service start/stop.
knotData = {
source = "$TANGLED_VM_DATA_DIR/knot";
target = "/mnt/knot-data";
···
target = "/var/log/spindle";
};
};
};
# This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall
networking.firewall.enable = false;
time.timeZone = "Europe/London";
services.getty.autologinUser = "root";
environment.systemPackages = with pkgs; [curl vim git sqlite litecli];
services.tangled.knot = {
···
};
};
};
users = {
# So we don't have to deal with permission clashing between
# blank disk VMs and existing state
···
nixpkgs.lib.nixosSystem {
inherit system;
modules = [
+
self.nixosModules.did-method-plc
+
self.nixosModules.jetstream
self.nixosModules.knot
self.nixosModules.spindle
({
···
diskSize = 10 * 1024;
cores = 2;
forwardPorts = [
+
# caddy
+
{
+
from = "host";
+
host.port = 80;
+
guest.port = 80;
+
}
+
{
+
from = "host";
+
host.port = 443;
+
guest.port = 443;
+
}
+
{
+
from = "host";
+
proto = "udp";
+
host.port = 443;
+
guest.port = 443;
+
}
# ssh
{
from = "host";
···
# as SQLite is incompatible with them. So instead we
# mount the shared directories to a different location
# and copy the contents around on service start/stop.
+
caddyData = {
+
source = "$TANGLED_VM_DATA_DIR/caddy";
+
target = config.services.caddy.dataDir;
+
};
knotData = {
source = "$TANGLED_VM_DATA_DIR/knot";
target = "/mnt/knot-data";
···
target = "/var/log/spindle";
};
};
+
useHostCerts = true;
};
# This is fine because any and all ports that are forwarded to host are explicitly marked above, we don't need a separate guest firewall
networking.firewall.enable = false;
+
# resolve `*.tngl.boltless.dev` to host
+
services.dnsmasq.enable = true;
+
services.dnsmasq.settings.address = "/tngl.boltless.dev/10.0.2.2";
time.timeZone = "Europe/London";
+
services.timesyncd.enable = lib.mkVMOverride true;
services.getty.autologinUser = "root";
environment.systemPackages = with pkgs; [curl vim git sqlite litecli];
services.tangled.knot = {
···
};
};
};
+
services.pds = {
+
enable = true;
+
# overriding package version to support emails
+
package = pkgs.pds.overrideAttrs (old: rec {
+
version = "0.4.188";
+
src = pkgs.fetchFromGitHub {
+
owner = "bluesky-social";
+
repo = "pds";
+
tag = "v${version}";
+
hash = "sha256-t8KdyEygXdbj/5Rhj8W40e1o8mXprELpjsKddHExmo0=";
+
};
+
pnpmDeps = pkgs.pnpm_9.fetchDeps {
+
inherit version src;
+
pname = old.pname;
+
sourceRoot = old.sourceRoot;
+
fetcherVersion = 2;
+
hash = "sha256-lQie7f8JbWKSpoavnMjHegBzH3GB9teXsn+S2SLJHHU=";
+
};
+
});
+
settings = {
+
LOG_ENABLED = "true";
+
+
PDS_JWT_SECRET = "8cae8bffcc73d9932819650791e4e89a";
+
PDS_ADMIN_PASSWORD = "d6a902588cd93bee1af83f924f60cfd3";
+
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX = "2e92e336a50a618458e1097d94a1db86ec3fd8829d7735020cbae80625c761d7";
+
+
PDS_EMAIL_SMTP_URL = envVarOr "TANGLED_VM_PDS_EMAIL_SMTP_URL" null;
+
PDS_EMAIL_FROM_ADDRESS = envVarOr "TANGLED_VM_PDS_EMAIL_FROM_ADDRESS" null;
+
+
PDS_DID_PLC_URL = "http://localhost:8080";
+
PDS_HOSTNAME = "pds.tngl.boltless.dev";
+
PDS_PORT = 3000;
+
};
+
};
+
services.plc.enable = true;
+
services.jetstream = {
+
enable = true;
+
livenessTtl = 300;
+
websocketUrl = "ws://localhost:3000/xrpc/com.atproto.sync.subscribeRepos";
+
};
+
services.caddy = {
+
enable = true;
+
configFile = pkgs.writeText "Caddyfile" ''
+
{
+
debug
+
cert_lifetime 3601d
+
pki {
+
ca local {
+
intermediate_lifetime 3599d
+
}
+
}
+
}
+
+
plc.tngl.boltless.dev {
+
tls internal
+
reverse_proxy http://localhost:8080
+
}
+
+
*.pds.tngl.boltless.dev, pds.tngl.boltless.dev {
+
tls internal
+
reverse_proxy http://localhost:3000
+
}
+
+
jetstream.tngl.boltless.dev {
+
tls internal
+
reverse_proxy http://localhost:6008
+
}
+
+
knot.tngl.boltless.dev {
+
tls internal
+
reverse_proxy http://localhost:6444
+
}
+
+
spindle.tngl.boltless.dev {
+
tls internal
+
reverse_proxy http://localhost:6555
+
}
+
'';
+
};
users = {
# So we don't have to deal with permission clashing between
# blank disk VMs and existing state