Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
266 lines (232 loc) · 13.2 KB
/
Copy path.env.example
File metadata and controls
266 lines (232 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# ──────────────────────────────────────────────────────
# Chronos Ledger: Environment Configuration Template
# Copy this file to .env and fill in your values.
# NEVER commit .env to version control.
# ──────────────────────────────────────────────────────
# Database
DATABASE_URL=postgresql://chronos_admin:change_me_password@chronos-db:5432/chronos_ledger
# Redis
REDIS_URL=redis://chronos-cache:6379/0
# JWT, generate with: openssl rand -hex 32
JWT_SECRET_SIGNING_KEY=replace_with_64_char_hex_secret_generated_by_openssl_rand_hex_32
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=15
# A refresh token is single use: spending one returns a new pair.
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
# The first password for the built-in administrator account. Nothing can
# log in as the administrator until this is set: the account is seeded with
# a hash of a random string that is thrown away, so this repository
# publishes no password for anyone to find. It is applied on boot while
# that account is still waiting for a password of its own, and does nothing
# once you have logged in and chosen one. An admin password reset puts the
# account back into that waiting state, so remove this line once you are
# in. setup.sh generates a value and prints it once.
INITIAL_ADMIN_PASSWORD=replace_with_the_first_admin_password
# The shortest password a person is allowed to choose: the first-login
# change, and any account an admin creates with a password they typed. The
# passwords the system generates for itself (the CSV importer, an admin
# reset) are random and not measured against this. Raise it if your policy
# says so; it refuses to go below 8.
PASSWORD_MIN_LENGTH=12
# Rate limits. Counted in Redis over a fixed window, on the three routes
# where hammering pays: sign-in, the visitor kiosk, and the calendar feed.
# Set any count to 0 to turn that one off, or RATE_LIMIT_ENABLED=false to
# turn off all three. Redis being unreachable turns them off too: it is a
# cache here and not a system of record, and an instance that cannot see it
# should keep letting people in rather than lock the organization out until
# it comes back.
RATE_LIMIT_ENABLED=true
# Failed sign-ins per 15 minutes, per calling address and per account. Both
# are counted, and a correct password costs neither.
RATE_LIMIT_LOGIN_PER_IP=10
RATE_LIMIT_LOGIN_PER_EMAIL=5
# Visitor check-ins per kiosk account per hour. Loose on purpose: a busy
# front desk is genuinely fast, and a limit that interrupts real work gets
# switched off, which is worse than a loose one.
RATE_LIMIT_GUEST_CHECKIN=300
# Calendar feed fetches per feed token per hour. Apple Calendar's fastest
# refresh is twelve an hour by itself, and somebody with a phone and a
# laptop is two of those.
RATE_LIMIT_CALENDAR_FEED=60
# Largest CSV an upload accepts, in MB. A bigger file is refused with 413
# before anything is imported. nginx refuses requests over 25 MB on its own,
# so going past 25 means raising client_max_body_size in nginx/nginx.conf too.
CSV_UPLOAD_MAX_MB=25
# How coarse a member's location fix may be, as a multiple of the session's
# fence radius. 2 refuses a fix coarser than 30 m on the default 15 m radius.
# 0 turns the check off.
GEOFENCE_ACCURACY_FACTOR=2
# How many days ahead the ledger is kept written, from tomorrow on. The 23:00
# job fills in whatever is missing, and a CSV import fills a new cycle at once.
# A day past a cycle's end date is never written. 1 to 366.
LEDGER_HORIZON_DAYS=28
# How many days a visitor check-in (name and phone number) is kept before a
# daily job at 03:30 deletes it. 0 keeps them forever. 90 is a common choice.
GUEST_RETENTION_DAYS=0
# How many hours an Idempotency-Key is remembered. A retry inside the window
# gets the first response back; a daily job at 03:45 deletes older records.
# 1 or more.
IDEMPOTENCY_RETENTION_HOURS=24
# Whether an admin may decide a visit that was not meant for them. Off, and
# the person the visitor came to see is the only one who can let them in,
# which stalls at the door when that person has gone home. On, an
# organization admin decides any visit and a unit admin those meant for
# their own unit, and the row records which of them did. A front desk
# attendant is not an admin: give the desk a guest-scoped API key on an
# admin account rather than an admin's password.
GUEST_ADMIN_CAN_DECIDE=false
# How long the visit code a check-in hands the visitor keeps opening
# GET /guest/visit/{code}, in minutes. Past it the lookup answers as
# though the code never existed. 0 leaves codes working for as long as
# the check-in is kept. 240 suits a code printed on a lobby slip.
GUEST_VISIT_CODE_TTL_MINUTES=0
# How long an approved visit lasts, in minutes, counted from the decision
# and not from the check-in. Past it the visit shows at
# GET /guest/overstays and a notice goes to the desk once. Chronos revokes
# nothing and turns nobody out: the row says the time is up and a person
# decides what to do about it. 0 leaves an approved visit open-ended.
GUEST_VISIT_LENGTH_MINUTES=0
# The most one extension may add to a visit, in minutes. More than this is
# refused rather than quietly trimmed, so a desk that meant 30 and typed
# 300 hears about it. A visit can be extended again as often as somebody
# keeps deciding to, so this bounds one grant and not the whole visit.
# 0 refuses every extension, for an organization that would rather the
# visitor checked in again.
GUEST_VISIT_EXTENSION_MAX_MINUTES=240
# Whether a change to a room or a booking is written down for an outside
# system to poll for, at GET /api/v1/changes/. Off, because it is a table
# that grows with every write and nothing deletes it, and an instance with
# no integrator has nobody to read it. Turning it on starts recording from
# that moment: a consumer reads the rooms and the bookings once in full and
# polls from there.
CHANGE_FEED_ENABLED=false
# Outgoing webhooks (docs/webhooks.md), sent from the change feed above, so
# they need it on. How often, in seconds, the worker fans new changes out and
# sends what is due, from 1 to 3600.
WEBHOOK_DELIVERY_INTERVAL_SECONDS=5
# Hosts a webhook may reach over plain http, comma separated, matched exactly
# and without the port. Empty, so every webhook URL must be https.
WEBHOOK_ALLOW_HTTP_HOSTS=
# Whether a webhook may go to a loopback, private or link-local address. Off,
# because otherwise a URL an admin types can make this server call into its
# own network. Turn it on only where the receiver really is on that network.
WEBHOOK_ALLOW_PRIVATE_TARGETS=false
# Whether a registered outside issuer may vouch for a person, so somebody
# already signed in there arrives here without a second password. Off, and off
# means the route answers 404 rather than 403: whether an instance has an
# integration is not something probing it should reveal. Turning it on is not
# enough on its own, since nothing verifies until an operator registers an
# issuer and a signing key. docs/federated-identity.md is the design.
FEDERATED_IDENTITY_ENABLED=false
# The longest life this instance will accept in a note from that issuer, in
# seconds, counted from when it arrives. The note says how long it should work
# and this is the second opinion: an issuer that sets that a year out has
# minted a bearer token for a year. Two minutes is long enough for a browser to
# hand a note over and short enough that a stolen one is worth little.
FEDERATED_TOKEN_MAX_LIFETIME_SECONDS=120
# How far this instance and that issuer may disagree about the time before a
# perfectly good note is refused, in seconds. It is not symmetric: a note may
# have expired this recently, or claim to have been minted this far ahead.
# Widening it widens the window a stolen note can be replayed in by the same
# amount, so an operator running NTP everywhere can set it lower.
FEDERATED_CLOCK_SKEW_SECONDS=60
# App
APP_ENV=production
APP_CORS_ORIGINS=http://localhost,http://localhost:3000
# Whether this instance serves its own API documentation at /docs, /redoc
# and /openapi.json. Left blank it follows APP_ENV: off in production, on
# everywhere else. The interactive docs are how somebody learns this API,
# and publishing the whole surface of a live instance is how somebody finds
# the parts of it they were not meant to reach. Set true to publish them
# anyway (an internal-only instance, say), or false to keep them off in
# development too.
DOCS_ENABLED=
# Which upstream addresses uvicorn will believe X-Forwarded-For from. The
# app container publishes no ports of its own, so nginx inside the compose
# network is the only way in and * costs nothing here. It matters because
# without it every request arrives from the proxy: the sign-in rate limit
# would count the whole organization as one caller, and ten failed attempts
# by one person would lock out everybody. Trusting the header is safe here
# because nginx overwrites it rather than appending to it, so a caller
# cannot name its own address. If you put another proxy in front of this
# stack, do not widen this: turn on nginx's realip module in
# nginx/chronos-common.conf so the address nginx sees is the real client.
FORWARDED_ALLOW_IPS=*
# Organization domain (used for cookie scope on intranet)
ORG_DOMAIN_MASK=org.internal
# What the interface calls the engine's six nouns, one variable each. Blank
# means the engine's own neutral word (Staff, Member, Activity, Unit, Lead,
# Cycle), so set as few or as many as your words need. Labels only: the
# schema, the API and the data are identical whatever you put here, so these
# are safe to change on a running instance.
#
# There are no presets. A domain does not agree with itself: two universities
# in one city will disagree on Course against Module, and "Faculty" is the
# unit at some institutions and the person at others. Expect to set all six.
# docs/vocabulary.md has the whole subject, including what labels never
# change: the API fields, the columns and the CSV headers stay as they are
# whatever the screen says.
LABEL_STAFF=
LABEL_MEMBER=
LABEL_ACTIVITY=
LABEL_UNIT=
LABEL_LEAD=
LABEL_CYCLE=
# Where the organization is, as an IANA name. This decides what "today" means:
# which sessions the dashboard shows, which day the nightly generator lays
# down, and when 23:00 is. A container runs UTC, so leaving this unset for an
# organization that is not in UTC puts the dashboard a whole offset out.
# An offset like +05:30 is refused, because it cannot know when daylight
# saving moves. The server is refused to start on a name no zone database
# knows, rather than quietly falling back.
ORG_TIMEZONE=UTC
# The clock the application reads (docs/clock.md). Leave these alone for a
# deployment. A host program embedding the engine for a simulation sets
# CLOCK_MODE=host and CLOCK_START to an ISO 8601 instant with an offset; the
# clock then stands still until the host advances it, and the nightly jobs run
# as it advances instead of on a schedule. ID_SEED, an integer, makes the
# identifiers that are not secrets repeat from one run to the next.
# CLOCK_MODE=system
# CLOCK_START=2026-09-07T08:00:00+00:00
# ID_SEED=
# Web Push (VAPID keys, generate with: npx web-push generate-vapid-keys)
VAPID_PUBLIC_KEY=your_vapid_public_key_here
VAPID_PRIVATE_KEY=your_vapid_private_key_here
VAPID_CONTACT_EMAIL=admin@org.internal
# Database password (used by both postgres container and backend DATABASE_URL)
DB_PASSWORD=change_me_password
# Frontend (injected at build time via Next.js)
NEXT_PUBLIC_API_URL=http://localhost/api/v1
NEXT_PUBLIC_WS_URL=ws://localhost/ws
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_vapid_public_key_here
# Telemetry (CFlair-Counter: Life-Experimentalist/CFlair-Counter): OPT-IN.
# Off by default: a stock install talks to nothing outside your network.
# To turn it on, set ENABLED=true *and* point ENDPOINT at a CFlair-Counter
# instance you run. Both must be set; an empty endpoint disables the pings.
NEXT_PUBLIC_TELEMETRY_ENABLED=false
NEXT_PUBLIC_TELEMETRY_ENDPOINT=
TELEMETRY_ENABLED=false
TELEMETRY_ENDPOINT=
# Deployment, read by docker-compose rather than by the app.
# EDGE_HTTP_PORT and EDGE_HTTPS_PORT are the HOST ports the edge proxy
# surfaces on; the container always listens on 80 and 443. Set them when
# another stack on the same host owns 80 and 443, so Chronos can sit behind
# an outer proxy instead of competing for the ports.
EDGE_HTTP_PORT=80
EDGE_HTTPS_PORT=443
# Cloudflare Tunnel, off unless the "tunnel" compose profile is on. For a
# host program on another machine that has to reach Chronos with no inbound
# port open here. The token is what the Cloudflare dashboard hands over when
# the tunnel is created; it is a credential, so it lives here and nowhere
# else, and there is no default. Turn the profile on with
# COMPOSE_PROFILES=tunnel, which compose reads from this file, so the usual
# `docker compose up -d` and `chronos up` then start the tunnel too.
# docs/deployment.md, "Cloudflare Tunnel".
CLOUDFLARE_TUNNEL_TOKEN=
# COMPOSE_PROFILES=tunnel
# Which published images docker-compose.prod.yml pulls. VERSION takes a tag
# or a digest; pin a digest for a reproducible deploy. GHCR_OWNER has to be
# lowercase even when the GitHub account is not, because a registry refuses a
# repository path with a capital in it.
GHCR_OWNER=life-experimentalist
VERSION=latest