Skip to content

Environment Variables ​

Application configuration through environment variables.

Backend Configuration ​

Create a .env file in the sis_backendgo/ directory:

bash
# Server
SERVER_PORT=8000
SERVER_HOST=0.0.0.0
ENVIRONMENT=production

# Database (PostgreSQL)
DB_HOST=localhost
DB_PORT=5432
DB_USER=sis_app
DB_PASSWORD=your-secure-password
DB_NAME=sis
DB_SSLMODE=require
DB_MAX_CONNS=25

# Session Store (Valkey/Redis)
REDIS_URL=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0

# Session Lifetime
SESSION_TTL=4h
SESSION_SLIDING_EXPIRATION=true

# Session Cookies
COOKIE_DOMAIN=sis.youruniversity.edu
COOKIE_PATH=/api
COOKIE_SECURE=true

# CORS
CORS_ALLOWED_ORIGINS=https://sis.youruniversity.edu
CORS_ALLOW_CREDENTIALS=true

# Photo Storage
PHOTO_STORAGE_PATH=/opt/sis/photos
PHOTO_MAX_SIZE_MB=2

# Email
SMTP_HOST=smtp.youruniversity.edu
SMTP_PORT=587
SMTP_USER=sis@youruniversity.edu
SMTP_PASSWORD=your-email-password
SMTP_FROM=noreply@youruniversity.edu
SMTP_USE_TLS=true
EMAIL_SEND_PER_MINUTE=120

# Role network rules (Administration > Role Networks)
ROLE_NETWORK_ENFORCEMENT=log
ROLE_NETWORK_BREAKGLASS_CIDRS=

Required Variables ​

Server ​

VariableDescriptionDefault
SERVER_PORTHTTP listen port8000
SERVER_HOSTHTTP listen address0.0.0.0
ENVIRONMENTRuntime environment (development or production)development

Database ​

VariableDescriptionDefault
DB_HOSTPostgreSQL server hostname127.0.0.1
DB_PORTPostgreSQL port5432
DB_USERDatabase usernamepostgres
DB_PASSWORDDatabase passwordpostgres
DB_NAMEDatabase namesis
DB_SSLMODESSL mode (disable, require, verify-full)disable
DB_MAX_CONNSMaximum database connections25

Session Store (Valkey/Redis) ​

VariableDescriptionDefault
REDIS_URLValkey/Redis server addresslocalhost:6379
REDIS_PASSWORDValkey/Redis password(empty)
REDIS_DBValkey/Redis database number0

Valkey or Redis is required for session management. The backend stores authenticated user sessions in Valkey/Redis and issues session cookies to the browser.

Session Lifetime ​

VariableDescriptionDefault
SESSION_TTLIdle-timeout ceiling: the longest a session may sit unused4h
SESSION_SLIDING_EXPIRATIONExtend the TTL on each requesttrue

A session's actual idle timeout is the smallest of three values:

  1. SESSION_TTL, the hard ceiling set here;
  2. Session Timeout on Administration > System Settings (SESSION_TIMEOUT_MINUTES);
  3. the Idle timeout of the role the user is currently acting as, set on Administration > Roles.

A role's limit can only shorten a session, never lengthen it. The limit follows the role the user is acting as, so switching to a role with a shorter limit tightens the session at once and switching back restores it. The server enforces all three. Edits reach sessions that are already signed in within about 30 seconds.

A 12-hour absolute cap is compiled in

Whatever SESSION_TTL says, and regardless of sliding expiration, a session ends 12 hours after it was created. That cap is not configurable. Setting SESSION_TTL=24h does not produce a 24-hour session.

There is no JWT anywhere in this system, and no JWT_ACCESS_TOKEN_LIFETIME variable. Sessions are opaque IDs in a cookie, resolved against Valkey on every request.

Session Cookies ​

VariableDescriptionDefault
COOKIE_DOMAINDomain for session cookies(empty)
COOKIE_PATHPath scope for session cookies/api
COOKIE_SECURERequire HTTPS for cookies (true or false)false

TIP

In production, always set COOKIE_SECURE=true to ensure session cookies are only sent over HTTPS.

CORS ​

VariableDescriptionDefault
CORS_ALLOWED_ORIGINSComma-separated allowed originshttp://localhost:5173
CORS_ALLOW_CREDENTIALSAllow credentials in CORStrue

Photo Storage ​

VariableDescriptionDefault
PHOTO_STORAGE_PATHDirectory for photo uploads./photos
PHOTO_MAX_SIZE_MBMaximum photo upload size in MB2

Official Photo Repository (SMB) ​

Credentials for the read-only ID-picture share that the photo sweep mirrors. Leaving these blank is a supported state: the sync screen then reports itself as unconfigured and the Run button stays disabled.

VariableDescriptionDefault
PHOTO_SMB_HOSTHostname or IP of the file server(blank)
PHOTO_SMB_PORTSMB port445
PHOTO_SMB_SHAREShare name, e.g. IDPictures$(blank)
PHOTO_SMB_DIROptional subdirectory; blank means the share root(blank)
PHOTO_SMB_USERAccount: UPN, DOMAIN\user, or bare name(blank)
PHOTO_SMB_PASSWORDAccount password(blank)
PHOTO_SMB_DOMAINDomain, if not folded into the user name(blank)
PHOTO_SYNC_CONCURRENCYParallel file transfers4

Only HOST, SHARE and USER decide whether the repository counts as configured (SMBConfig.Configured()); the rest may be blank. An empty value is indistinguishable from an unset one, because getEnv falls through to the default on either.

See Photo Repository for the sweep itself, the three accepted forms of the account name, and the dotenv quoting traps that silently corrupt a password.

Email (SMTP) ​

VariableDescriptionDefault
SMTP_HOSTSMTP server hostnamelocalhost
SMTP_PORTSMTP port1025
SMTP_USERSMTP username(empty)
SMTP_PASSWORDSMTP password(empty)
SMTP_FROMFallback sender while the EMAIL_NOREPLY_ADDRESS setting (Admin > System Settings) is unsetno-reply@puc.edu
SMTP_USE_TLSEnable TLSfalse
EMAIL_DELIVERYcapture or livecapture
EMAIL_SEND_PER_MINUTEThrottle for the staff-to-student email outbox (one message per recipient)120

Who may email students, how many at a time and from which addresses is configured in the application, not here; see Email Sending.

For development, use Mailpit (port 1025 SMTP, port 8025 web UI).

EMAIL_DELIVERY — which instance is allowed to mail real people ​

internal/shared/email dials whatever SMTP host it is handed and has no notion of environment, and SendAsync is fire-and-forget, so a failure is a log line nobody reads. Before this variable existed, the only thing standing between an instance and 238,000 real mailboxes was the SMTP_HOST value in a compose file.

  • capture (the default) — SMTP_HOST must be a known mail sink: mailpit, localhost, 127.0.0.1 or ::1. Anything else fails at startup.
  • live — real delivery. Requires ENVIRONMENT=production, and rejects a capture host so an instance cannot believe it is sending while mailpit eats the mail.

Both mismatches are fatal rather than warnings. A crash-loop is cheap and obvious; mail that should never have left is neither. Only the /records production instance is ever set to live.

Write mode ​

VariableDescriptionDefault
WRITE_MODEfull or restrictedfull

restricted is for a deployment that is reachable and real but does not yet own the data it serves — the /records instance, which runs alongside Colleague until the final migration. Safe methods pass; every other method is refused with 403 write_disabled unless the route is on the allowlist in internal/middleware/writegate.go.

That allowlist is a hardcoded Go slice, deliberately, and it is described here by category rather than by path so it stops going stale every time one is added — read the file for the current list, and TestWriteGateAllowsOnlyTheIntendedRoutes for the exact set of routes it permits:

CategoryWhy it is open
Authentication, the SAML assertion consumer, role switchingGate these and the instance cannot be logged into at all
Role and permission administrationSo a SUPER_ADMIN can still fix access on a locked-down instance; includes invalidate-cache
SAML identity provider administrationInstance authentication config, and /records is where SAML is wired
The workflow engine and the form responses it collectsRequests are the whole reason the instance is open for business; no Colleague counterpart
Reference data, extended-lookup types, and form-builder authoringThe institution's own configuration of the instance
Per-user UI state (dashboard layouts, shortcuts, filter presets)Blocking it makes the app feel broken while protecting nothing

Everything else is refused, including — deliberately — proxy invitations and proxy access, which hand a third party a view of a student's record.

Two things about the reference-data category are worth knowing before using it on a restricted instance, because the daily ODS importer resolves lookup rows by code:

  • The generic update endpoint cannot change a value's code at all. It is the lookup's identity, and renaming one would break the next nightly import quietly — per-row warnings and exit 0, not a visible failure.
  • Deleting a lookup value is possible and is not guarded that way. A row that is referenced fails on its NO ACTION foreign keys, but an unused row that an importer step hard-codes will delete cleanly and break the next run. This is why the grant is held to a single role.

It sits in the global middleware chain rather than inside protect, because the routes it most needs to cover are the ones permission nodes do not reach: SelfService and Public. Narrowing a role's grants to VIEW is the companion measure — it makes the UI hide affordances instead of offering them and failing — but grants are database rows editable from inside the application, so they cannot be the only thing enforcing this. An unrecognised value fails startup rather than falling back to either mode.

Role network rules ​

VariableDescriptionDefault
ROLE_NETWORK_ENFORCEMENToff, log or enforce — whether the rules on Administration > Role Networks refuse anythinglog
ROLE_NETWORK_BREAKGLASS_CIDRSComma-separated CIDRs (a bare address is one host) from which every role is usable whatever the rules say(empty)

Migration 291 lets each role be restricted to the networks it may be used from (see Role Networks). log, the default, evaluates the rules on every request and writes role_network_would_deny to the log for each user, role and address that enforce would refuse, but refuses nothing; it is how the real source ranges are confirmed before anyone is locked out. An empty value means log. Any other unrecognised value, or a break-glass entry that is not a valid address or CIDR, fails startup: guessing wrong in either direction is a security or an availability problem.

The address checked is the one the server settles on after Caddy, which overwrites X-Forwarded-For with the real peer. Users arriving over ZPA appear as the App Connector's address, not their own.

On the production host the two variables are set as SIS_ROLE_NETWORK_ENFORCEMENT and SIS_ROLE_NETWORK_BREAKGLASS_CIDRS in .env.local, shared by sdasis-backend and records-backend. The demo container hard-codes ROLE_NETWORK_ENFORCEMENT=off, deliberately not through the shared variable, because every role must work from wherever the demo is shown.

Frontend Configuration ​

For the Vue frontend, create .env.production:

bash
VITE_API_URL=https://sis.youruniversity.edu/api/v1
VITE_APP_TITLE=Student Information System

Environment-Specific Settings ​

Development ​

bash
ENVIRONMENT=development
DB_PORT=5432
DB_SSLMODE=disable
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
COOKIE_SECURE=false
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USE_TLS=false

Production ​

Production does not use sis_backendgo/.env

The prod stack runs in containers, and a container gets its environment from docker-compose.prod.yml, whose values are interpolated from a root .env.local on the server. sis_backendgo/.env does not exist there at all.

Two consequences. Host-side names are SIS_/SIS2_-prefixed (SIS_PHOTO_SMB_USER) and compose maps them onto the plain names in this page's tables; there is no prefix handling in the application, only those explicit compose lines. And a variable that is absent from .env.local usually fails silently -- most references carry an empty default, so the feature just reports itself as unconfigured. .env.local.example at the repo root is the tracked template listing every variable and how each one fails.

Host-built CLI tools (migrate-daily, sanitize-db, backfills) are the other path: they read unprefixed names via godotenv relative to their working directory, so on prod they must be handed their environment explicitly.

bash
ENVIRONMENT=production
DB_SSLMODE=require
CORS_ALLOWED_ORIGINS=https://sis.youruniversity.edu
COOKIE_DOMAIN=sis.youruniversity.edu
COOKIE_SECURE=true
SMTP_HOST=smtp.youruniversity.edu
SMTP_PORT=587
SMTP_USE_TLS=true

Security Best Practices ​

  1. Never commit .env files to version control
  2. Use strong passwords for database, Valkey/Redis, and email
  3. Set COOKIE_SECURE=true in production to prevent session hijacking
  4. Use HTTPS in production (required for secure cookies)
  5. Limit CORS origins to specific domains
  6. Enable DB SSL (DB_SSLMODE=require) in production
  7. Set ENVIRONMENT=production to disable development features
  8. Set a Valkey/Redis password in production environments

Verifying Configuration ​

Test your configuration:

bash
cd sis_backendgo
go build ./...
./api  # or: go run ./cmd/api/main.go
# Check: http://localhost:8000/health

Student Information System Documentation