···232232**Hold DID recovery/migration (did:plc):**
2332331. Back up `rotation.key` and DID string (from `did.txt` or plc.directory)
2342342. Set `database.did_method: plc` and `database.did: "did:plc:..."` in config
235235-3. Provide `rotation_key_path` — signing key auto-generates if missing
235235+3. Provide `rotation_key` (multibase K-256 private key) — signing key auto-generates if missing
2362364. On boot: `LoadOrCreateDID()` adopts the DID, `EnsurePLCCurrent()` auto-updates PLC directory if keys/URL changed
2372375. Without rotation key: hold boots but logs warning about PLC mismatch
238238
···5959 allow_all_crew: false
6060 # URL to fetch avatar image from during bootstrap.
6161 profile_avatar_url: https://atcr.io/web-app-manifest-192x192.png
6262+ # Bluesky profile display name. Synced on every startup.
6363+ profile_display_name: Cargo Hold
6464+ # Bluesky profile description. Synced on every startup.
6565+ profile_description: ahoy from the cargo hold
6266 # Post to Bluesky when users push images. Synced to captain record on startup.
6367 enable_bluesky_posts: false
6468 # Deployment region, auto-detected from cloud metadata or S3 config.
···7579 did: ""
7680 # PLC directory URL. Only used when did_method is 'plc'. Default: https://plc.directory
7781 plc_directory_url: https://plc.directory
7878- # Rotation key path for did:plc. Controls DID identity (separate from signing key). Defaults to {database.path}/rotation.key.
7979- rotation_key_path: ""
8282+ # Rotation key for did:plc in multibase format (starting with 'z'). Generate with: goat key generate. Supports K-256 and P-256 curves. Controls DID identity (separate from signing key).
8383+ rotation_key: ""
8084 # libSQL sync URL (libsql://...). Works with Turso cloud, Bunny DB, or self-hosted libsql-server. Leave empty for local-only SQLite.
8185 libsql_sync_url: ""
8286 # Auth token for libSQL sync. Required if libsql_sync_url is set.
+9-30
deploy/upcloud/cloudinit.go
···3636// values like client_name, owner_did, etc. are literal in the templates.
3737type ConfigValues struct {
3838 // S3 / Object Storage
3939- S3Endpoint string
4040- S3Region string
4141- S3Bucket string
3939+ S3Endpoint string
4040+ S3Region string
4141+ S3Bucket string
4242 S3AccessKey string
4343 S3SecretKey string
4444···112112}
113113114114// generateAppviewCloudInit generates the cloud-init user-data script for the appview server.
115115-func generateAppviewCloudInit(cfg *InfraConfig, vals *ConfigValues, goVersion string) (string, error) {
115115+// Sets up the OS, directories, config, and systemd unit. Binaries are deployed separately via SCP.
116116+func generateAppviewCloudInit(cfg *InfraConfig, vals *ConfigValues) (string, error) {
116117 naming := cfg.Naming()
117118118119 configYAML, err := renderConfig(appviewConfigTmpl, vals)
···133134 }
134135135136 return generateCloudInit(cloudInitParams{
136136- GoVersion: goVersion,
137137 BinaryName: naming.Appview(),
138138- BuildCmd: "appview",
139138 ServiceUnit: serviceUnit,
140139 ConfigYAML: configYAML,
141140 ConfigPath: naming.AppviewConfigPath(),
142141 ServiceName: naming.Appview(),
143142 DataDir: naming.BasePath(),
144144- RepoURL: cfg.RepoURL,
145145- RepoBranch: cfg.RepoBranch,
146143 InstallDir: naming.InstallDir(),
147144 SystemUser: naming.SystemUser(),
148145 ConfigDir: naming.ConfigDir(),
···152149}
153150154151// generateHoldCloudInit generates the cloud-init user-data script for the hold server.
155155-// When withScanner is true, a second phase is appended that builds the scanner binary,
156156-// creates scanner data directories, and installs a scanner systemd service.
157157-func generateHoldCloudInit(cfg *InfraConfig, vals *ConfigValues, goVersion string, withScanner bool) (string, error) {
152152+// When withScanner is true, a second phase is appended that creates scanner data
153153+// directories and installs a scanner systemd service. Binaries are deployed separately via SCP.
154154+func generateHoldCloudInit(cfg *InfraConfig, vals *ConfigValues, withScanner bool) (string, error) {
158155 naming := cfg.Naming()
159156160157 configYAML, err := renderConfig(holdConfigTmpl, vals)
···175172 }
176173177174 script, err := generateCloudInit(cloudInitParams{
178178- GoVersion: goVersion,
179175 BinaryName: naming.Hold(),
180180- BuildCmd: "hold",
181176 ServiceUnit: serviceUnit,
182177 ConfigYAML: configYAML,
183178 ConfigPath: naming.HoldConfigPath(),
184179 ServiceName: naming.Hold(),
185180 DataDir: naming.BasePath(),
186186- RepoURL: cfg.RepoURL,
187187- RepoBranch: cfg.RepoBranch,
188181 InstallDir: naming.InstallDir(),
189182 SystemUser: naming.SystemUser(),
190183 ConfigDir: naming.ConfigDir(),
···205198 return "", fmt.Errorf("scanner config: %w", err)
206199 }
207200208208- // Append scanner build and setup phase
201201+ // Append scanner setup phase (no build — binary deployed via SCP)
209202 scannerUnit, err := renderScannerServiceUnit(scannerServiceUnitParams{
210203 DisplayName: naming.DisplayName(),
211204 User: naming.SystemUser(),
···225218226219 scannerPhase := fmt.Sprintf(`
227220# === Scanner Setup ===
228228-echo "Building scanner..."
229229-cd %s/scanner
230230-CGO_ENABLED=1 go build \
231231- -ldflags="-s -w" \
232232- -trimpath \
233233- -o ../bin/%s ./cmd/scanner
234234-cd %s
235221236222# Scanner data dirs
237223mkdir -p %s/vulndb %s/tmp
···251237252238echo "=== Scanner setup complete ==="
253239`,
254254- naming.InstallDir(),
255255- naming.Scanner(),
256256- naming.InstallDir(),
257240 naming.ScannerDataDir(), naming.ScannerDataDir(),
258241 naming.SystemUser(), naming.SystemUser(), naming.ScannerDataDir(),
259242 naming.ScannerConfigPath(),
···267250}
268251269252type cloudInitParams struct {
270270- GoVersion string
271253 BinaryName string
272272- BuildCmd string
273254 ServiceUnit string
274255 ConfigYAML string
275256 ConfigPath string
276257 ServiceName string
277258 DataDir string
278278- RepoURL string
279279- RepoBranch string
280259 InstallDir string
281260 SystemUser string
282261 ConfigDir string