Issue 17: StartOS 0.4.0 packaging

- Dockerfile: multi-stage build (Node frontend + Python backend)
- StartOS manifest, interfaces, backups, version graph
- start.sh: auto-generates session secret, creates first approver on boot
- Backend serves built frontend as SPA in production
- Single data volume for SQLite DB, platform backups cover it
This commit is contained in:
Johnny 5
2026-06-07 19:23:26 +00:00
parent a70bdeaa5e
commit 89eddd2ad0
22 changed files with 636 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
import { sdk } from '../sdk'
// No custom actions for v1. Config is handled at install time.
export const actions = sdk.Actions.of()
+5
View File
@@ -0,0 +1,5 @@
import { sdk } from './sdk'
export const { createBackup, restoreInit } = sdk.setupBackups(
async ({ effects }) => sdk.Backups.ofVolumes('main'),
)
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from './sdk'
export const setDependencies = sdk.setupDependencies(async () => ({}))
+8
View File
@@ -0,0 +1,8 @@
export { createBackup } from './backups'
export { main } from './main'
export { init, uninit } from './init'
export { actions } from './actions'
import { buildManifest } from '@start9labs/start-sdk'
import { manifest as sdkManifest } from './manifest'
import { versionGraph } from './install/versionGraph'
export const manifest = buildManifest(versionGraph, sdkManifest)
+16
View File
@@ -0,0 +1,16 @@
import { sdk } from '../sdk'
import { setDependencies } from '../dependencies'
import { setInterfaces } from '../interfaces'
import { versionGraph } from '../install/versionGraph'
import { actions } from '../actions'
import { restoreInit } from '../backups'
export const init = sdk.setupInit(
restoreInit,
versionGraph,
setInterfaces,
setDependencies,
actions,
)
export const uninit = sdk.setupUninit(versionGraph)
+7
View File
@@ -0,0 +1,7 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { current, other } from './versions'
export const versionGraph = VersionGraph.of({
current,
other,
})
+2
View File
@@ -0,0 +1,2 @@
export { v_0_1_0 as current } from './v_0_1_0'
export const other = []
@@ -0,0 +1,16 @@
import { VersionInfo } from '@start9labs/start-sdk'
import * as fs from 'fs'
export const v_0_1_0 = VersionInfo.of({
version: '0.1.0:0',
releaseNotes: {
en_US: 'Initial release. Internal fund administration and quarterly valuation sign-off.',
},
migrations: {
up: async ({ effects }) => {
// DB initialization handled by Alembic on app startup.
// First approver account created by start.sh if no users exist.
},
down: async ({ effects }) => {},
},
})
+24
View File
@@ -0,0 +1,24 @@
import { sdk } from './sdk'
const appPort = 8000
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const uiMulti = sdk.MultiHost.of(effects, 'ui-multi')
const uiMultiOrigin = await uiMulti.bindPort(appPort, {
protocol: 'http',
})
const ui = sdk.createInterface(effects, {
name: 'Web UI',
id: 'ui',
description: 'Ten31Portal fund administration interface',
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
const uiReceipt = await uiMultiOrigin.export([ui])
return [uiReceipt]
})
+39
View File
@@ -0,0 +1,39 @@
import { sdk } from './sdk'
const appPort = 8000
export const main = sdk.setupMain(async ({ effects }) => {
console.info('Starting Ten31Portal')
const subcontainer = await sdk.SubContainer.of(
effects,
{ imageId: 'main' },
sdk.Mounts.of().mountVolume({
volumeId: 'main',
subpath: null,
mountpoint: '/data',
readonly: false,
}),
'ten31portal-sub',
)
return sdk.Daemons.of(effects).addDaemon('primary', {
subcontainer,
exec: {
command: ['sh', '-c', '/app/start.sh'],
env: {
TEN31_DB_PATH: '/data/portal.db',
TEN31_SESSION_SECRET: process.env.TEN31_SESSION_SECRET || 'change-me',
},
},
ready: {
display: 'Web Interface',
fn: () =>
sdk.healthCheck.checkPortListening(effects, appPort, {
successMessage: 'Ten31Portal is ready',
errorMessage: 'Ten31Portal is starting...',
}),
},
requires: [],
})
})
+32
View File
@@ -0,0 +1,32 @@
import { setupManifest } from '@start9labs/start-sdk'
export const manifest = setupManifest({
id: 'ten31portal',
title: 'Ten31Portal',
license: 'MIT',
packageRepo: 'https://github.com/smallblocks/Ten31Portal',
upstreamRepo: 'https://github.com/smallblocks/Ten31Portal',
marketingUrl: 'https://ten31.com',
donationUrl: 'https://ten31.com',
docsUrls: ['https://github.com/smallblocks/Ten31Portal/blob/main/README.md'],
description: {
short: 'Internal fund administration and valuation sign-off',
long: 'System of record for Ten31 entities, holdings, positions, and quarterly valuation sign-off. Replaces Carta for internal use.',
},
volumes: ['main'],
images: {
main: {
source: { dockerBuild: { dockerfile: './Dockerfile', workdir: '.' } },
arch: ['x86_64', 'aarch64'],
},
},
alerts: {
install: 'After installation, access the web UI to log in. A default approver account is created on first boot using the configured credentials.',
update: null,
uninstall: 'Uninstalling will remove all fund data. Create a backup first.',
restore: null,
start: null,
stop: null,
},
dependencies: {},
})
+4
View File
@@ -0,0 +1,4 @@
import { StartSdk } from '@start9labs/start-sdk'
import { manifest } from './manifest'
export const sdk = StartSdk.of().withManifest(manifest).build(true)