AlgoJudge documentation0.1

Configuration

Every setting the Server reads, what it defaults to, and the six that stop it from starting when they are missing or wrong.

The Server is configured by appsettings.json, by user secrets, and by environment variables — later ones override earlier ones. A deployment uses the environment.

Every setting has two spellings. The configuration key is Section:Key; the environment variable is the same thing with an AJ_ prefix and __ between the sections, because the Server registers its environment provider with that prefix. So Admin:Token is AJ_Admin__Token, and Storage:Stores:objects:Kind is AJ_Storage__Stores__objects__Kind.

`AJ_ADMIN_TOKEN` is not one of them

It looks like a Server variable and is not. The Server reads Admin:Token and nothing else. AJ_ADMIN_TOKEN is a Compose variable, substituted into AJ_Admin__Token by AlgoJudge-Ops and by the development stack, which is why every page under Install and operate uses the short name.

An installation configured by hand, without those Compose files, sets AJ_Admin__Token.

The database

KeyEnvironmentDefault
ConnectionStrings:DbConnectionStringAJ_ConnectionStrings__DbConnectionStringa local algojudge database
Database:MigrateOnStartAJ_Database__MigrateOnStartfalse

Database:MigrateOnStart is off here and on in Ops. Outside Development the Server refuses to start while a migration is pending unless it is set — applying a schema change to a production database stays a decision somebody makes. Ops ships MIGRATE_ON_START=true because it takes a backup first, in that order. See Updating.

Both contexts read it: the Server and the LTI module keep separate migration histories in one database, so a value that reached only one of them still refuses the start.

The switch exists because nothing else shipped can apply a migration. aj-admin has no migrate command and the image carries no SDK, and the alternative needs a workstation with the source — which a self-hosted stack does not have. Without the switch, a fresh installation would have its migration pending and never start.

It takes a PostgreSQL advisory lock while it works, and that is not decoration. Several instances against one database is a supported arrangement and they start together after an update; EF Core has no migration lock of its own, so without this one of them dies on a duplicate history row.

Where the files go

KeyEnvironmentDefault
Storage:Stores:<id>:KindAJ_Storage__Stores__<id>__Kindrequiredpostgres, filesystem or s3
Storage:Stores:<id>:Path…__Pathrequired for filesystem
Storage:Stores:<id>:Endpoint…__Endpointrequired for s3
Storage:Stores:<id>:Bucket…__Bucketrequired for s3
Storage:Stores:<id>:AccessKey, …:SecretKey…__AccessKey, …__SecretKeyrequired for s3
Storage:Stores:<id>:Region…__Regionus-east-1
Storage:Stores:<id>:CreateBucket…__CreateBucketfalse
Storage:Stores:<id>:TimeoutSeconds…__TimeoutSeconds600
Storage:Stores:<id>:MaxErrorRetry…__MaxErrorRetry2
Storage:DefaultAJ_Storage__Defaultobjects
Storage:SpoolPathAJ_Storage__SpoolPatha directory under the system temporary directory
Storage:Migration:StartHourUtcAJ_Storage__Migration__StartHourUtc2
Storage:Migration:BudgetMinutes…__BudgetMinutes30
Storage:Migration:GraceMinutes…__GraceMinutes60

<id> is a name a deployment chooses. Several stores may be configured at once, including several of one kind, and a stored file remembers which one holds it for ever — so a store id may never be reused for a different location.

Storage:SpoolPath is where the postgres and s3 stores rest an upload while its length becomes known. In a container the default is inside the image's own layer; point it at a volume if packages are large.

The operator surface

KeyEnvironmentDefault
Admin:TokenAJ_Admin__Tokenempty — /admin/** is closed

The token is checked on top of the loopback requirement, in the X-AlgoJudge-Admin-Token header. An empty or whitespace value closes the whole group, including the endpoint that sets the administrator's password. See The admin surface.

KeyEnvironmentDefault
DataProtection:KindAJ_DataProtection__Kinddatabase
DataProtection:Certificates:<n>:PathAJ_DataProtection__Certificates__0__Pathnone
DataProtection:Certificates:<n>:Password…__Passwordempty

ephemeral is refused outside Development, and an unrecognised value is refused everywhere. The certificate list rotates by prepending: the first entry encrypts, every entry decrypts, and dropping an old one makes existing keys unreadable. Backup says what this means for a database dump.

Whose word to take for an address

KeyEnvironmentDefault
Forwarded:KnownProxiesAJ_Forwarded__KnownProxiesnone — one of these two is required
Forwarded:KnownNetworksAJ_Forwarded__KnownNetworksnone

Addresses go in the first, networks in the second, and the Server will not read one as the other. A network with bits below its prefix is refused by name, because 10.0.0.5/24 reads two ways and this list decides whose word is taken for every visitor's address. The literal none is a complete answer for a Server reached directly.

Both accept a comma-separated list.

Addresses this Server advertises

KeyEnvironmentDefault
App:BaseUrlAJ_App__BaseUrlempty — a relative path is used
PublicApiUrlAJ_PublicApiUrlderived from the request
Cors:AllowedOriginsAJ_Cors__AllowedOrigins__0, __1, …https://localhost:5173, from the shipped appsettings.json. With no origins at all, no CORS policy is added

App:BaseUrl is where the Client is served from; it ends a federated sign-in and an LTI launch by sending the browser back. PublicApiUrl is the address given to LTI platforms. Leave both unset where the Client and the Server share an origin.

Where they do not, the two origins must still be the same site — one registrable domain and one scheme — because the session cookie is SameSite=Lax and a browser will not keep it across sites. An allowlist entry does not change that: CORS decides whether a call may read an answer, SameSite decides whether the cookie is sent at all. What two origins require has the measurement.

`Cors:AllowedOrigins` is a list, and a comma-separated one binds nothing

It is bound as an array, so it needs indexed variables — AJ_Cors__AllowedOrigins__0=https://algojudge.example. Writing AJ_Cors__AllowedOrigins=a,b binds zero origins, silently, and every cross-origin call then fails in the browser rather than in a log.

Forwarded:KnownProxies and Forwarded:KnownNetworks directly above do accept commas. Two adjacent settings, two different rules.

The rest

KeyEnvironmentDefault
Limits:MaxRequestBytesAJ_Limits__MaxRequestBytes134217728 (128 MiB)
Preconfiguration:PathAJ_Preconfiguration__Pathunset — the feature is off and its endpoints say so
Events:SendTimeoutSecondsAJ_Events__SendTimeoutSeconds5
Maintenance:ForceAfterSecondsAJ_Maintenance__ForceAfterSeconds300
Files:CollectAtHourUtcAJ_Files__CollectAtHourUtc6
Retention:SessionOriginDaysAJ_Retention__SessionOriginDays30
Retention:SubmissionOriginDaysAJ_Retention__SubmissionOriginDays365
Problems:ReservedSlugPrefixesAJ_Problems__ReservedSlugPrefixes__0, …none
UvaExplorer:OriginAJ_UvaExplorer__Originthe hosted problem picker
AllowedHostsAJ_AllowedHosts*

ASPNETCORE_ENVIRONMENT carries no AJ_ prefix — it is the framework's, and it is what decides whether the demo world is seeded, whether Swagger is mapped, and whether two of the refusals below apply at all.

Seven ways the Server refuses to start

Each is deliberate, and each replaces a failure that would otherwise arrive much later and look like something else.

  1. No storage is configured. Where a product puts its files is not something to inherit from a default nobody read.
  2. Storage:Default names a store that is not configured. Uploads would otherwise fail one at a time.
  3. Neither Forwarded:KnownProxies nor Forwarded:KnownNetworks is set. Trusting every sender of X-Forwarded-For lets a visitor state their own address; trusting only loopback quietly records the proxy. There is no defensible default, so there is none.
  4. DataProtection:Kind is ephemeral outside Development. Every restart would sign everybody out, and a second instance could not read the first's cookie.
  5. DataProtection:Certificates is configured while the kind is ephemeral. Nothing is stored for a certificate to encrypt. One of the two was meant, and guessing which would leave an installation believing its keys are protected while nothing is being kept at all.
  6. DataProtection:Kind is a value this Server does not know. Including redis, which is refused by name: it would be a second stateful service to back up and restore, and this product does not ask an installation for one.
  7. A migration is pending outside Development. The only one an operator can wave through, and Database:MigrateOnStart is how.

A named DataProtection:Certificates entry this Server cannot use — no Path, a file that is not there, one it cannot open, one carrying no private key — is refused at startup as well. A key ring that cannot be read is an installation nobody can sign in to, and it should say so while somebody is still watching the deployment rather than at the first sign-in.

On this page