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
@@ -16,3 +16,7 @@ frontend/dist/
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
# Deploy
deploy/node_modules/
deploy/javascript/
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 smallblocks Copyright (c) 2026 Ten31
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+15
View File
@@ -1,9 +1,13 @@
"""Ten31Portal FastAPI application.""" """Ten31Portal FastAPI application."""
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import FileResponse
from ten31portal.config import SESSION_SECRET from ten31portal.config import SESSION_SECRET
from ten31portal.db_init import run_migrations from ten31portal.db_init import run_migrations
@@ -38,6 +42,17 @@ def health() -> dict[str, str]:
return {"status": "ok"} return {"status": "ok"}
# Serve built frontend in production (when static/ dir exists next to the app)
_static_dir = Path(__file__).resolve().parent.parent / "static"
if _static_dir.is_dir():
@app.get("/{path:path}")
async def serve_spa(path: str):
file = _static_dir / path
if file.is_file():
return FileResponse(file)
return FileResponse(_static_dir / "index.html")
def cli() -> None: def cli() -> None:
import uvicorn import uvicorn
uvicorn.run("ten31portal.main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("ten31portal.main:app", host="0.0.0.0", port=8000, reload=True)
+2
View File
@@ -0,0 +1,2 @@
node_modules/
javascript/
+40
View File
@@ -0,0 +1,40 @@
# --- Frontend build stage ---
FROM node:20-slim AS frontend-build
WORKDIR /build/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --include=dev
COPY frontend/ ./
RUN npx vite build
# --- Backend stage ---
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python deps
COPY backend/pyproject.toml ./
COPY backend/ten31portal/ ./ten31portal/
COPY backend/alembic/ ./alembic/
COPY backend/alembic.ini ./
RUN pip install --no-cache-dir .
# Copy built frontend
COPY --from=frontend-build /build/frontend/dist ./static/
# Startup script
COPY deploy/start.sh ./start.sh
RUN chmod +x ./start.sh
# Data volume mount point
RUN mkdir -p /data
ENV TEN31_DB_PATH=/data/portal.db
ENV TEN31_SESSION_SECRET=change-me
EXPOSE 8000
CMD ["./start.sh"]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

+345
View File
@@ -0,0 +1,345 @@
{
"name": "ten31portal-startos",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ten31portal-startos",
"version": "0.1.0",
"dependencies": {
"@start9labs/start-sdk": "^0.4.0-beta.58"
},
"devDependencies": {
"@types/node": "^22.19.0",
"@vercel/ncc": "^0.38.4",
"typescript": "^5.9.3"
}
},
"node_modules/@iarna/toml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz",
"integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==",
"license": "ISC"
},
"node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodable/entities": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
"integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nodable"
}
],
"license": "MIT"
},
"node_modules/@start9labs/start-sdk": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0.tgz",
"integrity": "sha512-PFfO7tV9nzQFZL3KXaZyf16C5VZtM+dCDlRhLHpmwssTKtcjyCEhBrB9locuS2yFqu69rj+5kLFzCWDHeRRibg==",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
"@noble/curves": "^1.8.2",
"@noble/hashes": "^1.7.2",
"@types/ini": "^4.1.1",
"deep-equality-data-structures": "^2.0.0",
"fast-xml-parser": "^5.5.6",
"ini": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
"mime": "^4.0.7",
"yaml": "^2.7.1",
"zod": "^4.3.6",
"zod-deep-partial": "^1.2.0"
}
},
"node_modules/@types/ini": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz",
"integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz",
"integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@vercel/ncc": {
"version": "0.38.4",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
"dev": true,
"license": "MIT",
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/deep-equality-data-structures": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz",
"integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==",
"license": "MIT",
"dependencies": {
"object-hash": "^3.0.0"
}
},
"node_modules/fast-xml-builder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.5.0",
"xml-naming": "^0.1.0"
}
},
"node_modules/fast-xml-parser": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz",
"integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"@nodable/entities": "^2.1.0",
"fast-xml-builder": "^1.2.0",
"path-expression-matcher": "^1.5.0",
"strnum": "^2.3.0",
"xml-naming": "^0.1.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/ini": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
"integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/isomorphic-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
"node_modules/mime": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
"funding": [
"https://github.com/sponsors/broofa"
],
"license": "MIT",
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/path-expression-matcher": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/strnum": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
"integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/xml-naming": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-deep-partial": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz",
"integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==",
"license": "MIT",
"peerDependencies": {
"zod": "^4.1.13"
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "ten31portal-startos",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
"check": "tsc --noEmit"
},
"dependencies": {
"@start9labs/start-sdk": "^0.4.0-beta.58"
},
"devDependencies": {
"@types/node": "^22.19.0",
"@vercel/ncc": "^0.38.4",
"typescript": "^5.9.3"
}
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
set -e
# Generate session secret if not set or default
if [ "$TEN31_SESSION_SECRET" = "change-me" ]; then
if [ -f /data/.session-secret ]; then
export TEN31_SESSION_SECRET=$(cat /data/.session-secret)
else
export TEN31_SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
echo "$TEN31_SESSION_SECRET" > /data/.session-secret
chmod 600 /data/.session-secret
fi
fi
# Create first approver on first boot if no users exist
if [ ! -f /data/.initialized ]; then
echo "First boot: running migrations and creating default approver..."
# Set default credentials (user can change via CLI later)
ADMIN_NAME="${TEN31_ADMIN_NAME:-Admin}"
ADMIN_EMAIL="${TEN31_ADMIN_EMAIL:-admin@ten31.com}"
ADMIN_PASSWORD="${TEN31_ADMIN_PASSWORD:-changeme}"
python3 -m ten31portal.cli create-user \
--name "$ADMIN_NAME" \
--email "$ADMIN_EMAIL" \
--role approver \
--password "$ADMIN_PASSWORD" || true
touch /data/.initialized
echo "Default approver created: $ADMIN_EMAIL"
fi
# Serve frontend static files from the backend
exec uvicorn ten31portal.main:app \
--host 0.0.0.0 \
--port 8000 \
--log-level info
+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)
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"outDir": "./javascript",
"rootDir": "./startos"
},
"include": ["startos/**/*.ts"]
}