- Rails 8.1 monolith for the Raspberry Pi Code Editor API (REST + GraphQL), served at
editor-api.raspberrypi.org. - Primary runtime via Docker; API listens on port 3009.
- REST under
app/controllers/api/**with Jbuilder views inapp/views/api/**; GraphQL at/graphql(schema inapp/graphql/**). - Auth: Browser/session via OmniAuth (OIDC to Hydra); API token via
Authorization: BearerwithIdentifiable#identify_user→User.from_token→HydraPublicApiClient. - Authorization: cancancan in
app/models/ability.rb. Useload_and_authorize_resourcein controllers; GraphQL usesTypes::ProjectType.authorized?andcurrent_abilityin context. - Domain:
Project(+Component) with Active Storage attachments. Domain operations inlib/concepts/**(e.g.Project::Create,Project::CreateRemix). Prefer calling these from controllers/mutations. - Jobs: GoodJob (
bundle exec good_job start --max-threads=8). Admin UI at/admin/good_job. - Integrations: Profile API (
lib/profile_api_client.rb), UserInfo API, GitHub GraphQL (lib/github_api.rb), GitHub webhooks viaGithubWebhooksController. - Storage/CORS: Active Storage uses S3 in non-dev. CORS via
config/initializers/cors.rbandlib/origin_parser.rb.CorpMiddlewaresets CORP for Active Storage routes.
- GraphQL context:
current_user,current_ability,remix_origin. Object IDs use GlobalID. Locale fallback viaProjectLoader:[requested, 'en', nil]. - REST pagination returns HTTP
Linkheader (seeApi::ProjectsController#pagination_link_header). - Project rules: identifiers unique per locale; default component name/extension immutable on update; students cannot update
instructionson school projects; creating a project in a school auto-buildsSchoolProject. - Remix:
Project::CreateRemixclones media/components, setsremix_origin, clearslesson_id. - Errors: domain ops return
OperationResponsewith:error; controllers return 4xx heads; GraphQL raisesGraphQL::ExecutionError. Exceptions reported to Sentry. - snake_case for variable numbers (exceptions:
sha256,X-Hub-Signature-256).
cp .env.example .env
docker compose build
docker compose run --rm api rails db:setup
docker compose up- Use
docker composefor all commands; project mounts intoeditor-api:builderwith tmpfs fortmp/.
- Full suite:
docker compose run --rm api rspec - Single spec:
docker compose run --rm api rspec spec/path/to/spec.rb - Lint:
docker compose run --rm api bundle exec rubocop - CI: GitHub Actions with Ruby 4, Postgres 12, Redis.
- Salesforce sync specs need
SALESFORCE_CONNECT_DBset and matching Heroku Connect tables (schema comes from the publishedheroku-connectimage after Salesforce mapping is exported).
- Sync writes to the
salesforce_connectDB (not a Salesforce API). Pattern from editor-api PR #677. - Feature flag:
SALESFORCE_ENABLED=true. - After deploy, backfill:
rails salesforce_sync:school,salesforce_sync:role,salesforce_sync:contact,salesforce_sync:school_class,salesforce_sync:class_teacher,salesforce_sync:lesson. - Parent-sync race guard (required for any job using
__r__external-ID lookups). Heroku Connect rejects an INSERT permanently withForeign key external ID … not foundif the parent record isn't yet in Salesforce — the mirror row staysFAILEDforever (no auto-retry). Callensure_parent_synced!(model, external_id_field, external_id, label)onSalesforce::SalesforceSyncJob(the base class) before saving a child record; it checks the parent has a non-nilsfidin its Heroku Connect mirror and raisesSalesforceRecordNotFoundif not. The base job declaresretry_on SalesforceRecordNotFound, wait: :polynomially_longer, attempts: 10so the job self-heals once parents land. SeeSalesforce::RoleSyncJobandSalesforce::ClassTeacherSyncJobfor call-site examples.
- Routes:
config/routes.rb. Auth:config/initializers/omniauth.rb,app/helpers/authentication_helper.rb,app/controllers/concerns/identifiable.rb. - Permissions:
app/models/ability.rb. Domain ops:lib/concepts/**. Models:app/models/**. GraphQL:app/graphql/**.
- Never commit secrets (
.env,config/master.key, API tokens, webhook secrets). .env.examplecontains placeholder values only.