first commit

This commit is contained in:
2026-08-08 21:14:10 -05:00
commit e1600da8b1
55 changed files with 2687 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
.git
.ai-workspace
node_modules
javascript
*.s9pk
log/*
tmp/*
storage/*
test
+24
View File
@@ -0,0 +1,24 @@
/.bundle
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
/storage/*
!/storage/.keep
/node_modules
/javascript
/*.s9pk
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Local AI workspace
.ai-workspace/
+1
View File
@@ -0,0 +1 @@
3.4.5
+23
View File
@@ -0,0 +1,23 @@
FROM ruby:3.4.5-slim-bookworm
WORKDIR /app
RUN apt-get update -qq && apt-get install --no-install-recommends -y \
build-essential curl libpq-dev libyaml-dev \
&& rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle config set without "development test" && \
bundle install && \
rm -rf /usr/local/bundle/cache
COPY . .
RUN chmod +x bin/rails bin/rake
ENV RAILS_ENV=production \
RAILS_LOG_TO_STDOUT=true \
RAILS_SERVE_STATIC_FILES=true \
PORT=3000
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
+16
View File
@@ -0,0 +1,16 @@
source "https://rubygems.org"
ruby ">= 3.3.0", "< 3.5"
gem "rails", "~> 8.0.2"
gem "pg", "~> 1.5"
gem "puma", ">= 5.0"
gem "bootsnap", require: false
group :development, :test do
gem "debug", platforms: %i[mri windows], require: "debug/prelude"
end
group :test do
gem "minitest-reporters"
end
+339
View File
@@ -0,0 +1,339 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.5.1)
actionpack (= 8.0.5.1)
activesupport (= 8.0.5.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.5.1)
actionpack (= 8.0.5.1)
activejob (= 8.0.5.1)
activerecord (= 8.0.5.1)
activestorage (= 8.0.5.1)
activesupport (= 8.0.5.1)
mail (>= 2.8.0)
actionmailer (8.0.5.1)
actionpack (= 8.0.5.1)
actionview (= 8.0.5.1)
activejob (= 8.0.5.1)
activesupport (= 8.0.5.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.5.1)
actionview (= 8.0.5.1)
activesupport (= 8.0.5.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.5.1)
actionpack (= 8.0.5.1)
activerecord (= 8.0.5.1)
activestorage (= 8.0.5.1)
activesupport (= 8.0.5.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.5.1)
activesupport (= 8.0.5.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.5.1)
activesupport (= 8.0.5.1)
globalid (>= 0.3.6)
activemodel (8.0.5.1)
activesupport (= 8.0.5.1)
activerecord (8.0.5.1)
activemodel (= 8.0.5.1)
activesupport (= 8.0.5.1)
timeout (>= 0.4.0)
activestorage (8.0.5.1)
actionpack (= 8.0.5.1)
activejob (= 8.0.5.1)
activerecord (= 8.0.5.1)
activesupport (= 8.0.5.1)
marcel (~> 1.0)
activesupport (8.0.5.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ansi (1.6.0)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
bootsnap (1.24.6)
msgpack (~> 1.2)
builder (3.3.0)
concurrent-ruby (1.3.8)
connection_pool (3.0.2)
crass (1.0.7)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
reline (>= 0.3.8)
drb (2.2.3)
erb (6.0.7)
erubi (1.13.1)
globalid (1.4.0)
activesupport (>= 6.1)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
logger (1.7.0)
loofah (2.25.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.1)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.2.1)
mini_mime (1.1.5)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
minitest-reporters (1.8.0)
ansi
builder
minitest (>= 5.0, < 7)
ruby-progressbar
msgpack (1.8.4)
net-imap (0.6.6)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.19.4-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-musl)
racc (~> 1.4)
pg (1.6.3)
pg (1.6.3-aarch64-linux)
pg (1.6.3-aarch64-linux-musl)
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
pp (0.6.4)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
puma (8.0.2)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.6)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.3.1)
rack (>= 3)
rails (8.0.5.1)
actioncable (= 8.0.5.1)
actionmailbox (= 8.0.5.1)
actionmailer (= 8.0.5.1)
actionpack (= 8.0.5.1)
actiontext (= 8.0.5.1)
actionview (= 8.0.5.1)
activejob (= 8.0.5.1)
activemodel (= 8.0.5.1)
activerecord (= 8.0.5.1)
activestorage (= 8.0.5.1)
activesupport (= 8.0.5.1)
bundler (>= 1.15.0)
railties (= 8.0.5.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.7.1)
loofah (~> 2.25, >= 2.25.2)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (8.0.5.1)
actionpack (= 8.0.5.1)
activesupport (= 8.0.5.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rake (13.4.2)
rbs (4.1.2)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
reline (0.7.0)
io-console (~> 0.5)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
thor (1.5.0)
timeout (0.6.1)
tsort (0.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.1.1)
useragent (0.16.11)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.8.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
bootsnap
debug
minitest-reporters
pg (~> 1.5)
puma (>= 5.0)
rails (~> 8.0.2)
CHECKSUMS
actioncable (8.0.5.1) sha256=5adb700c605a7ef7628f87dc7a6da20cd5f0ceac782a59055c864ea51a77d7c7
actionmailbox (8.0.5.1) sha256=f8b72eadf53b3e285df8f2d1f6533012abf5a0a001180abe436aea3139eeaed6
actionmailer (8.0.5.1) sha256=c3d2b3f96e1989ea25f51699786a97fcb2536eb7abfc2a667cb8f2376ec08403
actionpack (8.0.5.1) sha256=a5595c9d824d68884ddc4d3965ab78c897760d3752e190df7efe897371caa1eb
actiontext (8.0.5.1) sha256=370e90d35feb4313fc18ccef658776427d5bdd13126f266933b828a77e2125b2
actionview (8.0.5.1) sha256=472a108b9cc2295c4ac3ff09b028045e619875801f48c556f0085210b9cb1440
activejob (8.0.5.1) sha256=142407a21b6c3cbc6ddd92ca111ac18ea5c40298eb94d81845cd897a072a6880
activemodel (8.0.5.1) sha256=559be32aa9c40db7a3ee0aef926d4508a9ebd22f96f7276c11326d21a7dff4a4
activerecord (8.0.5.1) sha256=9252968fce404d75eb17092498a440d472167f2f8deee32b4658d6552b1eeea7
activestorage (8.0.5.1) sha256=239742932b2fdcf0ead175e0889dbd385a36da2168fd7bde023aaad88ef745f2
activesupport (8.0.5.1) sha256=329a4280c4fbcfcf338ae2cb9df28b0b14527929dba105e10b3604516d998710
ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bootsnap (1.24.6) sha256=c60bab88c70332290f0a2636a288f675299eb4f804a02a3c085b42eca9da164a
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918
mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8
marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
msgpack (1.8.4) sha256=4411c22d350dd1c20250f7eada3cca2695438c2f769cf0782f0cd065d90a3e7b
net-imap (0.6.6) sha256=96aa4ee50df3060203e649efc341f53480b791d49e150f2fdebf68beb141a8df
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af
nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca
nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb
nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527
nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551
nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a
nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b
pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99
pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea
pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c
pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f
pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6
pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d
pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
rails (8.0.5.1) sha256=c91cefbf38881876ddbe67b5e0246eb985d27d60c6bb1cd7034b4261de0ba495
rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d
rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2
railties (8.0.5.1) sha256=da1958e1d9dab04691a2f8721b3ff7fab323715d37f103c19972dedfd644d5c7
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rbs (4.1.2) sha256=050eb1d8b508f1233bed929c0f2c7052302f7adf295230d9cb314e9024078f48
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5
RUBY VERSION
ruby 4.0.6
BUNDLED WITH
4.0.16
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Todo Back contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
ARCHES := x86
include node_modules/@start9labs/start-sdk/s9pk.mk
+41
View File
@@ -0,0 +1,41 @@
# Todo Back
Todo Back is a deliberately small Rails JSON API packaged as a native StartOS 0.4 service. Its purpose is to validate the complete local-build → `.s9pk` → sideload workflow before investing in a larger application.
## API
- `GET /up` — Rails and PostgreSQL health check
- `GET /todos` — list todos
- `GET /todos/:id` — fetch one todo
- `POST /todos` — create with `{ "todo": { "title": "...", "completed": false } }`
- `PATCH /todos/:id` — update title and/or completion state
- `DELETE /todos/:id` — delete a todo
## Architecture
The package builds the Rails application from the root `Dockerfile` and runs PostgreSQL 17 as a second StartOS subcontainer. PostgreSQL data, Rails local storage, and generated secrets are persisted in separate StartOS volumes. On every start, PostgreSQL becomes healthy, `bin/rails db:prepare` runs, and then Puma starts on port 3000. StartOS publishes that port as an API interface and terminates TLS.
`SECRET_KEY_BASE` and the database password are generated during installation and stored in the backed-up config volume. Backups use the SDK PostgreSQL dump integration and also include config and local storage.
## Development
Install application dependencies and run tests:
```sh
bundle install
bin/rails db:prepare
bin/rails test
```
Build the x86_64 sideload package from inside a StartOS packaging workspace:
```sh
npm install
make x86
```
The resulting `todo-back_x86_64.s9pk` can be uploaded from StartOS under **System → Sideload Service**.
## Status
This is an unauthenticated MVP intended for sideload testing on a trusted network. Add authentication before exposing its interface beyond trusted gateways.
+3
View File
@@ -0,0 +1,3 @@
require_relative "config/application"
Rails.application.load_tasks
+5
View File
@@ -0,0 +1,5 @@
# Updating
The Rails application is developed in this repository. Update Ruby and gems deliberately in `Gemfile`, `.ruby-version`, `Gemfile.lock`, and `Dockerfile` together.
The PostgreSQL image is pinned in `startos/manifest/index.ts`. Before changing it, confirm the exact official image tag exists and review PostgreSQL upgrade requirements. Update `startos/versions/current.ts` and its release notes for every package release.
@@ -0,0 +1,9 @@
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
private
def render_not_found
render json: { error: "not_found" }, status: :not_found
end
end
+8
View File
@@ -0,0 +1,8 @@
class HealthController < ActionController::API
def show
ActiveRecord::Base.connection.select_value("SELECT 1")
head :ok
rescue ActiveRecord::ActiveRecordError
head :service_unavailable
end
end
+41
View File
@@ -0,0 +1,41 @@
class TodosController < ApplicationController
before_action :set_todo, only: %i[show update destroy]
def index
render json: Todo.order(created_at: :desc)
end
def show
render json: @todo
end
def create
todo = Todo.new(todo_params)
todo.save!
render json: todo, status: :created
rescue ActiveRecord::RecordInvalid => error
render json: { errors: error.record.errors }, status: :unprocessable_entity
end
def update
@todo.update!(todo_params)
render json: @todo
rescue ActiveRecord::RecordInvalid => error
render json: { errors: error.record.errors }, status: :unprocessable_entity
end
def destroy
@todo.destroy!
head :no_content
end
private
def set_todo
@todo = Todo.find(params[:id])
end
def todo_params
params.expect(todo: %i[title completed])
end
end
+3
View File
@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
+3
View File
@@ -0,0 +1,3 @@
class Todo < ApplicationRecord
validates :title, presence: true, length: { maximum: 255 }
end
+3
View File
@@ -0,0 +1,3 @@
# Package assets
No additional package assets are required by the MVP.
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "rake"
Rake.application.run
+4
View File
@@ -0,0 +1,4 @@
require_relative "config/environment"
run Rails.application
Rails.application.load_server
+16
View File
@@ -0,0 +1,16 @@
require_relative "boot"
require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "action_controller/railtie"
Bundler.require(*Rails.groups)
module TodoBack
class Application < Rails::Application
config.load_defaults 8.0
config.api_only = true
end
end
+4
View File
@@ -0,0 +1,4 @@
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup"
require "bootsnap/setup" if ENV.fetch("RAILS_ENV", "development") == "production"
+16
View File
@@ -0,0 +1,16 @@
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
url: <%= ENV["DATABASE_URL"] %>
development:
<<: *default
database: todo_back_development
test:
<<: *default
database: todo_back_test
production:
<<: *default
+3
View File
@@ -0,0 +1,3 @@
require_relative "application"
Rails.application.initialize!
+8
View File
@@ -0,0 +1,8 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
config.enable_reloading = true
config.eager_load = false
config.consider_all_requests_local = true
config.active_support.deprecation = :log
end
+14
View File
@@ -0,0 +1,14 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
config.enable_reloading = false
config.eager_load = true
config.consider_all_requests_local = false
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
config.log_tags = [:request_id]
config.logger = ActiveSupport::TaggedLogging.logger($stdout)
config.active_support.report_deprecations = false
config.assume_ssl = true
config.force_ssl = false
config.hosts.clear
end
+6
View File
@@ -0,0 +1,6 @@
Rails.application.configure do
config.enable_reloading = false
config.eager_load = ENV["CI"].present?
config.consider_all_requests_local = true
config.action_dispatch.show_exceptions = :rescuable
end
+6
View File
@@ -0,0 +1,6 @@
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV", "development")
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]
plugin :tmp_restart
+4
View File
@@ -0,0 +1,4 @@
Rails.application.routes.draw do
get "up" => "health#show", as: :rails_health_check
resources :todos, only: %i[index show create update destroy]
end
@@ -0,0 +1,9 @@
class CreateTodos < ActiveRecord::Migration[8.0]
def change
create_table :todos do |t|
t.string :title, null: false
t.boolean :completed, null: false, default: false
t.timestamps
end
end
end
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Todo Back">
<rect width="128" height="128" rx="28" fill="#1f2937"/>
<path d="M35 65l18 18 40-43" fill="none" stroke="#34d399" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

+15
View File
@@ -0,0 +1,15 @@
# Todo Back
Todo Back provides a small REST API for testing a native StartOS service installation.
After starting the service, open **Interfaces** and copy the Todo API URL. Use `/todos` to manage items. For example:
```sh
curl "$TODO_API/todos"
curl -X POST "$TODO_API/todos" \
-H 'Content-Type: application/json' \
-d '{"todo":{"title":"Test StartOS sideload","completed":false}}'
```
The service has no login in this MVP. Keep the interface limited to trusted gateways. PostgreSQL data and generated internal secrets are included in StartOS backups.
+1670
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "todo-back-startos",
"private": true,
"scripts": {
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
"prettier": "prettier --write startos",
"check": "tsc --noEmit"
},
"dependencies": {
"@start9labs/start-sdk": "2.0.9"
},
"overrides": {
"@start9labs/start-sdk": "$@start9labs/start-sdk"
},
"devDependencies": {
"@types/node": "^22.19.0",
"@vercel/ncc": "^0.38.4",
"prettier": "^3.6.2",
"typescript": "^6.0.3"
},
"prettier": {
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
}
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from '../sdk'
export const actions = sdk.Actions.of()
+23
View File
@@ -0,0 +1,23 @@
import { storeJson } from './fileModels/store.json'
import { sdk } from './sdk'
import { databaseName, databaseUser } from './utils'
export const { createBackup, restoreInit } = sdk.setupBackups(async () =>
sdk.Backups.withPgDump({
imageId: 'postgres',
dbVolume: 'postgres',
mountpoint: '/var/lib/postgresql/data',
pgdataPath: '',
database: databaseName,
user: databaseUser,
password: async () => {
const password = await storeJson
.read(value => value.databasePassword)
.once()
if (!password) throw new Error('Database password is not initialized')
return password
},
})
.addVolume('config')
.addVolume('storage'),
)
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from './sdk'
export const setDependencies = sdk.setupDependencies(async () => ({}))
+12
View File
@@ -0,0 +1,12 @@
import { FileHelper, z } from '@start9labs/start-sdk'
import { sdk } from '../sdk'
const shape = z.object({
databasePassword: z.string().min(32).catch(''),
secretKeyBase: z.string().min(64).catch(''),
})
export const storeJson = FileHelper.json(
{ base: sdk.volumes.config, subpath: 'store.json' },
shape,
)
+14
View File
@@ -0,0 +1,14 @@
export const DEFAULT_LANG = 'en_US'
const dict = {
Database: 0,
'The database is not ready': 1,
'Todo API': 2,
'The Todo API is ready': 3,
'The Todo API is not ready': 4,
'REST API for todo items': 5,
} as const
export type I18nKey = keyof typeof dict
export type LangDict = Record<(typeof dict)[I18nKey], string>
export default dict
@@ -0,0 +1,8 @@
import { LangDict } from './default'
export default {
es_ES: { 0: 'Base de datos', 1: 'La base de datos no está lista', 2: 'API de tareas', 3: 'La API de tareas está lista', 4: 'La API de tareas no está lista', 5: 'API REST para tareas' },
de_DE: { 0: 'Datenbank', 1: 'Die Datenbank ist nicht bereit', 2: 'Aufgaben-API', 3: 'Die Aufgaben-API ist bereit', 4: 'Die Aufgaben-API ist nicht bereit', 5: 'REST-API für Aufgaben' },
pl_PL: { 0: 'Baza danych', 1: 'Baza danych nie jest gotowa', 2: 'API zadań', 3: 'API zadań jest gotowe', 4: 'API zadań nie jest gotowe', 5: 'REST API dla zadań' },
fr_FR: { 0: 'Base de données', 1: "La base de données n'est pas prête", 2: 'API de tâches', 3: "L'API de tâches est prête", 4: "L'API de tâches n'est pas prête", 5: 'API REST pour les tâches' },
} satisfies Record<string, LangDict>
+5
View File
@@ -0,0 +1,5 @@
import { setupI18n } from '@start9labs/start-sdk'
import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
import translations from './dictionaries/translations'
export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
+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 './versions'
export const manifest = buildManifest(versionGraph, sdkManifest)
+18
View File
@@ -0,0 +1,18 @@
import { actions } from '../actions'
import { restoreInit } from '../backups'
import { setDependencies } from '../dependencies'
import { setInterfaces } from '../interfaces'
import { sdk } from '../sdk'
import { versionGraph } from '../versions'
import { seedSecrets } from './seedSecrets'
export const init = sdk.setupInit(
restoreInit,
versionGraph,
setInterfaces,
setDependencies,
actions,
seedSecrets,
)
export const uninit = sdk.setupUninit(versionGraph)
+18
View File
@@ -0,0 +1,18 @@
import { utils } from '@start9labs/start-sdk'
import { storeJson } from '../fileModels/store.json'
import { sdk } from '../sdk'
export const seedSecrets = sdk.setupOnInit(async (effects, kind) => {
if (kind !== 'install') return
await storeJson.merge(effects, {
databasePassword: utils.getDefaultString({
charset: 'a-z,A-Z,0-9',
len: 48,
}),
secretKeyBase: utils.getDefaultString({
charset: 'a-z,A-Z,0-9',
len: 128,
}),
})
})
+31
View File
@@ -0,0 +1,31 @@
import { i18n } from './i18n'
import { sdk } from './sdk'
import { appPort } from './utils'
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const host = sdk.MultiHost.of(effects, 'api')
const origin = await host.bindPort(appPort, {
protocol: 'http',
preferredExternalPort: 80,
addSsl: {
alpn: { specified: ['http/1.1'] },
preferredExternalPort: 443,
addXForwardedHeaders: true,
auth: null,
},
})
const api = sdk.createInterface(effects, {
name: i18n('Todo API'),
id: 'api',
description: i18n('REST API for todo items'),
type: 'api',
masked: false,
schemeOverride: null,
username: null,
path: '/',
query: {},
})
return [await origin.export([api])]
})
+97
View File
@@ -0,0 +1,97 @@
import { storeJson } from './fileModels/store.json'
import { i18n } from './i18n'
import { sdk } from './sdk'
import { appPort, databaseName, databaseUser } from './utils'
export const main = sdk.setupMain(async ({ effects }) => {
const secrets = await storeJson.read().once()
if (!secrets?.databasePassword || !secrets.secretKeyBase) {
throw new Error('Todo Back secrets have not been initialized')
}
const databaseUrl = `postgresql://${databaseUser}:${secrets.databasePassword}@127.0.0.1:5432/${databaseName}`
const appEnv = {
DATABASE_URL: databaseUrl,
SECRET_KEY_BASE: secrets.secretKeyBase,
RAILS_ENV: 'production',
RAILS_LOG_TO_STDOUT: 'true',
PORT: String(appPort),
}
const postgresSub = sdk.SubContainer.of(
effects,
{ imageId: 'postgres' },
sdk.Mounts.of().mountVolume({
volumeId: 'postgres',
subpath: null,
mountpoint: '/var/lib/postgresql/data',
readonly: false,
}),
'postgres-subcontainer',
)
const appSub = sdk.SubContainer.of(
effects,
{ imageId: 'app' },
sdk.Mounts.of().mountVolume({
volumeId: 'storage',
subpath: null,
mountpoint: '/app/storage',
readonly: false,
}),
'app-subcontainer',
)
return sdk.Daemons.of(effects)
.addDaemon('postgres', {
subcontainer: postgresSub,
exec: {
command: sdk.useEntrypoint(['-c', 'listen_addresses=127.0.0.1']),
env: {
POSTGRES_DB: databaseName,
POSTGRES_USER: databaseUser,
POSTGRES_PASSWORD: secrets.databasePassword,
PGDATA: '/var/lib/postgresql/data',
},
},
ready: {
display: i18n('Database'),
fn: () =>
sdk.healthCheck.runHealthScript(
[
'pg_isready',
'-h',
'127.0.0.1',
'-U',
databaseUser,
'-d',
databaseName,
],
postgresSub,
{ errorMessage: i18n('The database is not ready') },
),
},
requires: [],
})
.addOneshot('database-prepare', {
subcontainer: appSub,
exec: { command: ['bin/rails', 'db:prepare'], env: appEnv },
requires: ['postgres'],
})
.addDaemon('app', {
subcontainer: appSub,
exec: {
command: ['bundle', 'exec', 'puma', '-C', 'config/puma.rb'],
env: appEnv,
},
ready: {
display: i18n('Todo API'),
fn: () =>
sdk.healthCheck.checkWebUrl(effects, `http://127.0.0.1:${appPort}/up`, {
successMessage: i18n('The Todo API is ready'),
errorMessage: i18n('The Todo API is not ready'),
}),
},
requires: ['database-prepare'],
})
})
+15
View File
@@ -0,0 +1,15 @@
export const short = {
en_US: 'A minimal, self-hosted JSON API for todo items.',
es_ES: 'Una API JSON mínima y autohospedada para tareas.',
de_DE: 'Eine minimale, selbst gehostete JSON-API für Aufgaben.',
pl_PL: 'Minimalne, samodzielnie hostowane API JSON dla zadań.',
fr_FR: 'Une API JSON minimale et auto-hébergée pour les tâches.',
}
export const long = {
en_US: 'Create, read, update, and delete todo items through a small Rails API backed by PostgreSQL.',
es_ES: 'Crea, consulta, actualiza y elimina tareas mediante una pequeña API Rails respaldada por PostgreSQL.',
de_DE: 'Erstellen, lesen, aktualisieren und löschen Sie Aufgaben über eine kleine Rails-API mit PostgreSQL.',
pl_PL: 'Twórz, odczytuj, aktualizuj i usuwaj zadania przez małe API Rails oparte na PostgreSQL.',
fr_FR: 'Créez, consultez, modifiez et supprimez des tâches via une petite API Rails utilisant PostgreSQL.',
}
+25
View File
@@ -0,0 +1,25 @@
import { setupManifest } from '@start9labs/start-sdk'
import { long, short } from './i18n'
export const manifest = setupManifest({
id: 'todo-back',
title: 'Todo Back',
license: 'MIT',
packageRepo: 'https://github.com/alextab93/todo-back',
upstreamRepo: 'https://github.com/alextab93/todo-back',
marketingUrl: 'https://github.com/alextab93/todo-back',
donationUrl: null,
description: { short, long },
volumes: ['config', 'postgres', 'storage'],
images: {
app: {
source: { dockerBuild: {} },
arch: ['x86_64'],
},
postgres: {
source: { dockerTag: 'postgres:17.6-bookworm' },
arch: ['x86_64'],
},
},
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)
+3
View File
@@ -0,0 +1,3 @@
export const appPort = 3000
export const databaseName = 'todo_back_production'
export const databaseUser = 'todo_back'
+16
View File
@@ -0,0 +1,16 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const current = VersionInfo.of({
version: '0.1.1:0',
releaseNotes: {
en_US: 'Fix PostgreSQL readiness checks and HTTP/1.1 proxying; health now verifies the database.',
es_ES: 'Corrige las comprobaciones de PostgreSQL y el proxy HTTP/1.1; la salud ahora verifica la base de datos.',
de_DE: 'Korrigiert PostgreSQL-Prüfungen und HTTP/1.1-Proxying; der Status prüft nun die Datenbank.',
pl_PL: 'Poprawia test PostgreSQL i proxy HTTP/1.1; kontrola stanu sprawdza teraz bazę danych.',
fr_FR: 'Corrige les contrôles PostgreSQL et le proxy HTTP/1.1 ; la santé vérifie désormais la base de données.',
},
migrations: {
up: async () => {},
down: IMPOSSIBLE,
},
})
+4
View File
@@ -0,0 +1,4 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { current } from './current'
export const versionGraph = VersionGraph.of({ current, other: [] })
+7
View File
@@ -0,0 +1,7 @@
require "test_helper"
class TodoTest < ActiveSupport::TestCase
test "requires a title" do
assert_not Todo.new(title: "").valid?
end
end
+3
View File
@@ -0,0 +1,3 @@
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "@start9labs/start-sdk/tsconfig.base.json",
"include": ["startos/**/*.ts", "node_modules/**/startos"]
}