- Framework: Roda (routing tree web toolkit) with Sequel ORM and SQLite
- Auth: Rodauth (login, email auth/magic links, password reset, lockout)
- Server: Falcon (async Ruby web server), using
falcon servewith--threaded - CSS: Tailwind CSS, compiled via
tailwindcss-rubygem - Assets: Roda assets plugin with precompilation (
assets/compiled_assets.json) - Ruby version: Defined in
.ruby-version
git clone git@github.com:stephaniewilkinson/yonderbook.git
cd yonderbook
cp .env-example .env # if you msg me I can share my api keys
bundle install
rake db:migrate
falcon
Production (Render):
sqlite3 /var/data/production.dbDevelopment:
sqlite3 db/development.dbbundle exec rake test
Tests require environment variables — copy .env-example to .env and fill in values.
app.rb— Main Roda application class with routing, plugins, and Rodauth configconfig.ru— Rack config; loads Sentry, sets up env-specific middlewareRakefile— Definesprecompile,tailwind:build,tailwind:watch, and loadslib/tasks/*.rakelib/database.rb— Sequel/SQLite setup; creates DB constant, path depends onRACK_ENVlib/tasks/db.rake— Database rake tasks (migrate, reset, create_migration)
TODO: Clearly display the Goodreads name or logo on any location where Goodreads data appears. For instance if you are displaying Goodreads reviews, they should either be in a section clearly titled "Goodreads Reviews", or each review should say "Goodreads review from John: 4 of 5 stars..."
TODO: Link back to the page on Goodreads where the data data appears. For instance, if displaying a review, the name of the reviewer and a "more..." link at the end of the review must link back to the review detail page. You may not nofollow this link.
BookMooch is a book trading community where users can give away books they no longer need and receive books they want.
The BookMooch API allows up to 10 requests/second. Exceeding this results in 302 redirect responses (not standard 429s). In practice, keeping requests concurrent with a connection pool limit (rather than throttling with a rate limiter) works best — a leaky bucket limiter causes timeouts and connection issues with BookMooch's server.
All API calls accept parameters via either GET (URL params) or POST (body). Use POST for large payloads like bulk ASIN/ISBN submissions — GET has a ~2048 character URL limit, so large ISBN lists must be batched. POST can send arbitrarily large fields in a single request.
Errors are indicated by a negative result_code field in the XML response, with a result_text description:
<?xml version="1.0" encoding="UTF-8"?>
<userids>
<userid>
<id>john_smith</id>
<result_code>-1</result_code>
<result_text>no data found</result_text>
</userid>
</userids>The /api/userbook endpoint uses HTTP Basic Auth. A 302 response means rate limiting; a 401 or HTML error page means invalid credentials (users should use their BookMooch username, not email).
Deployed on Render with a persistent disk for SQLite at /var/data/production.db.
Render does not use the Procfile — commands are set in the dashboard under Settings:
Build command:
bundle install && bundle exec rake precompile
Start command:
bundle exec rake db:migrate && bundle exec falcon --verbose serve --threaded -n 2 -b http://0.0.0.0:${PORT}
- Render's persistent disk (
/var/data) is only mounted at runtime, not during builds. Migrations must run in the start command. - Rake tasks in
lib/tasks/must notrequiredatabase.rbat the top level — it callsFileUtils.mkdir_p('/var/data')which fails during builds. Require it lazily inside task bodies that need it. - The
precompiletask uses a bare Roda class (not the full App) to avoid loading all app dependencies during the build.app.rbalso callscompile_assetsat startup. tailwindcss-rubymust stay in the top-level Gemfile group (not:development) because it's needed by the build step.
This app uses the roda-route-list plugin. This makes all the routes available in a /routes.json file.
openssl req -x509 -out localhost.crt -keyout localhost.key \
-newkey rsa:2048 -nodes -sha256 \
-subj '/CN=localhost' -extensions EXT -config <( \
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")