Configuration¶
Stagemgr reads its configuration from three places, and which one a setting belongs in is not a matter of taste:
| Kind of setting | Where |
|---|---|
| Non-secret deployment settings -- host names, mount point, business rules, email addresses, house facts | config/server.yml |
| Connection details and secrets | the process environment (.env in development and test) or encrypted credentials |
| Anything the box office maintains -- theaters, venues, productions, users | the database, through the admin UI |
Scaffolding the files¶
The real config files are gitignored; the repository tracks .example
templates. One task copies them:
bundle exec rake setup:config
It creates each config/*.yml from its config/*.yml.example, plus .env from
.env.example, and never overwrites a file that already exists. It does not
load Rails -- it cannot, since config/database.yml is one of the files it
produces. bin/docker-entrypoint runs it on every container start.
| Generated | Read by | Notes |
|---|---|---|
config/database.yml |
Active Record | Every value is environment-driven; the same file works native, in Docker and in CI |
config/server.yml |
config/environments/{development,production}.rb |
The main deployment file, documented below |
config/ticket_print.yml |
config.x.tktprint |
Ticket-printing service URL, per environment |
config/schedule.yml |
resque-scheduler | Already tracked, so setup:config leaves it alone |
config/membership_card_spec.yml |
MembershipCards::Spec |
Member ID card geometry (spec); edit the copy to tune placement without touching the tracked example |
config/my_emma_credentials.yml |
nothing | A dead legacy file. MyEmma credentials come from MY_EMMA_* or credentials |
.env |
dotenv (development, test) and docker compose |
The rest of the setup: namespace¶
| Task | Boots Rails? | Does |
|---|---|---|
setup:config |
no | The scaffolding above |
setup:site[slug] |
no | Copies sites/example/ to sites/<slug>/ and inserts site_theme: <slug> under all: in config/server.yml (Theming) |
setup:secret_key_base |
no | Generates SECRET_KEY_BASE in .env if it is blank or missing; never replaces an existing value |
setup:bootstrap |
yes | Creates this environment's database, loads db/schema.rb, applies pending migrations, seeds only when there are no users |
setup:admin |
yes | Creates or updates the first administrator (ADMIN_EMAIL/ADMIN_PASSWORD skip the prompts) |
setup:theater |
yes | Creates the first theater and venue and associates the administrator |
setup:payments |
yes | Prompts for Stripe keys and writes them to .env |
setup:demo_data |
yes | One sample production with performances, ticket classes and allocations |
setup:doctor |
yes | Health check; exits 1 on problems |
setup:wizard |
yes | All of the above, interactively, in order |
Every task is idempotent: re-running one reports what already exists instead of failing.
config:setup is deprecated
The old name still works and forwards to setup:config, printing a
deprecation notice. Use setup:config.
Why setup:bootstrap and not db:prepare
db:prepare only reaches for db/schema.rb when it hits NoDatabaseError.
The Docker stack hands Rails a database MySQL has already created
(MYSQL_DATABASE), so db:prepare sees an existing-but-empty database and
replays every migration back to 2009 -- slow, fragile on MySQL 8, and it
rewrites the committed db/schema.rb on the way out. setup:bootstrap
loads the schema instead, and migrates only what is actually pending.
.env¶
.env is gitignored and read by two different things: docker compose, and
dotenv-rails -- which is loaded in the development and test groups only. In
production the same variables must come from the process environment (Compose
environment:, Passenger SetEnv, a systemd EnvironmentFile).
Delete blank lines, do not leave them
A variable set to the empty string is ignored by
AppSecrets rather than shadowing a credential -- that is
deliberate, and it is the fix for the outage described in
Troubleshooting.
But a blank line is still noise, and setup:doctor warns about every one it
finds: five in a .env copied straight from the template, four once
setup:secret_key_base (or the wizard) has filled in SECRET_KEY_BASE. If
you are not using a key, delete the line.
Database¶
| Variable | Default | Purpose |
|---|---|---|
DATABASE_USER |
stagemgr_dev |
MySQL account. Must not be root -- the mysql image refuses to start with MYSQL_USER=root |
DATABASE_PASSWORD |
password |
Its password |
DATABASE_HOST |
127.0.0.1 |
Compose overrides this to mysql |
DATABASE_PORT |
3306 |
|
DATABASE_NAME |
stagemgr_dev |
Development/production database |
TEST_DATABASE_NAME |
stagemgr_test |
RSpec and Cucumber. Give every concurrent checkout its own -- the suites truncate every table between examples |
DATABASE_POOL |
5 |
Active Record connection pool |
DATABASE_SOCKET |
-- | Connect over a Unix socket instead of TCP (a native Homebrew MySQL) |
MYSQL_ROOT_PASSWORD |
changeme-root |
Compose only: the MySQL container's root password. Deliberately not DATABASE_PASSWORD |
In production the database name still defaults (stagemgr_production) but the
user and password do not: config/database.yml.example uses ENV.fetch without
a default there, so booting production against stagemgr_dev/password because
a variable was unset fails loudly instead.
Redis¶
| Variable | Default | Purpose |
|---|---|---|
REDIS_URL |
redis://127.0.0.1:6379/0 |
Resque queues and Rack::Attack counters. Compose overrides it to redis://redis:6379/0 |
Rails¶
| Variable | Purpose |
|---|---|
RAILS_ENV |
development, test or production |
SECRET_KEY_BASE |
Session/cookie signing key. rake setup:secret_key_base fills it in; bundle exec rails secret generates one by hand |
RAILS_MASTER_KEY |
Alternative to shipping config/master.key/config/credentials/<env>.key as a file. See Credentials |
STAGEMGR_SUB_URI |
Sub-path the app is mounted at, e.g. /tickets. Compose passes it through as RAILS_RELATIVE_URL_ROOT. Optional: server.yml's sub_uri fills the setting when the variable is absent or blank, and the two must agree when both are set |
RAILS_SERVE_STATIC_FILES |
Serve public/ from Rails instead of from Nginx/Apache |
RAILS_LOG_TO_STDOUT |
Log to stdout for a container or journald log driver |
RAILS_MAX_THREADS, WEB_CONCURRENCY, PORT |
Puma only; the Docker image runs Passenger |
RAILS_RAISE_ERRORS |
Re-raise controller exceptions instead of rendering the friendly error page (app/controllers/application_controller.rb) |
BACKTRACE |
Unsilenced backtraces |
SKIP_REQUIRED_SECRETS_CHECK |
Let the production boot check pass with secrets missing. Only for a build or maintenance process that never serves a request |
Secrets¶
Each of these is also readable from encrypted credentials -- see Credentials & secrets for the full registry and the resolution order.
| Variable | Needed when |
|---|---|
STRIPE_SECRET_KEY |
payment_processing.default_gateway (or default_recurring_gateway) is stripe |
STRIPE_SIGNING_SECRET |
You receive Stripe webhooks (whsec_…) |
POSTMARK_API_TOKEN |
email.delivery_method is postmark |
RESQUE_ADMIN_PASSWORD |
Always, in production -- see the Resque dashboard |
MY_EMMA_USERNAME, MY_EMMA_PASSWORD, MY_EMMA_ACCOUNT_ID |
Using MyEmma mailing lists. All three are required together; with any of them absent the integration disables itself |
LGL_API_KEY |
Syncing customers with Little Green Light (Little Green Light sync). Optional: absent, the integration disables itself; it also needs littlegreenlight.sync set to enabled or readonly |
LGL_USER, LGL_PASSWORD |
The LGL donor-tier report link (littlegreenlight.donor_tier_report_url) asks for a login. Optional: Basic auth is sent only when both LGL_USER and LGL_PASSWORD are set |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
ActiveStorage on S3 (uncomment the matching block in config/storage.yml). Blank means local disk |
PAYPAL_LOGIN, PAYPAL_PASSWORD, PAYPAL_SIGNATURE, PAYPAL_PEM_FILE, PAYPAL_EXPRESS_LOGIN, PAYPAL_EXPRESS_PASSWORD |
Legacy PayPal gateways only |
Development runs MyEmma read-only
config/environments/development.rb calls MyEmma.read_only! after setting
credentials: reads work, writes are logged and skipped, whatever account the
credentials point at.
Little Green Light sync¶
Stagemgr links each customer to a constituent in the default theater's LGL
account; see Little Green Light Sync for what it
does. Sync runs only when littlegreenlight.sync is enabled or readonly
and LGL_API_KEY (or the littlegreenlight.api_key credential) is set.
Otherwise every LGL trigger, job and page button is a no-op, and the LGL Sync
admin page says why. The nightly donor-tier report download is the one
exception: it needs only donor_tier_report_url (below), not the API key or
the sync mode.
From LGL the integration reads constituents (by id, external id and email, and
the list of every constituent id), the constituents changed since the last
poll, and each linked constituent's gifts, which set the LGL Lapsed tag
(LYBUNT / SYBUNT) by fiscal year. The fiscal year starts on
theater.fiscal_year_start_month (see
the theater: block; January when unset).
The API key is a secret (above); the rest lives in the
littlegreenlight: section of config/server.yml:
all:
littlegreenlight:
sync: readonly # enabled | readonly | disabled
writable_address_ids: # optional, readonly only: 12, 34 or a YAML list
web_url: https://youraccount.littlegreenlight.com
donor_tier_report_url: # optional: permanent link of the LGL donor-tier saved report
donor_flag_excluded_tiers: # optional: ["Up to $99.99"] tiers that earn no receipt heart
| Key | ENV override | Purpose |
|---|---|---|
sync |
LGL_SYNC |
The sync mode, one of the three below. Case and surrounding spaces are ignored; any other word, or none, means disabled |
writable_address_ids |
LGL_WRITABLE_ADDRESS_IDS |
Comma-separated (or YAML list) Stagemgr address ids that may still be written to LGL in readonly mode, for acceptance testing. Ignored in enabled mode |
web_url |
-- | Base URL of the LGL account, used to link an LGL ID to the constituent page. Blank shows the id as plain text |
donor_tier_report_url |
LGL_DONOR_TIER_REPORT_URL |
Permanent link of the LGL saved report (CSV with External constituent ID, TG Tier Last Fiscal, TG Tier This Fiscal) that LglDonorLevelsFetch downloads nightly and imports as donor levels. Blank turns the nightly import off. Independent of sync and LGL_API_KEY; the import's owner and result-email recipient is the user whose email is email.addresses.supervisor_notifications |
donor_flag_excluded_tiers |
-- | Donor tier labels that do not earn the heart icon on box office receipts, copied exactly as this install's LGL names them, e.g. ["Up to $99.99"]. A YAML list or a single label. Any other tier for the current or last fiscal year earns the heart; unset means every tier does. Independent of sync and LGL_API_KEY |
A non-blank environment variable overrides the server.yml value; a blank one
counts as absent, the same rule as AppSecrets.
sync |
Reads from LGL, local tags and linking | Writes to LGL |
|---|---|---|
disabled (or missing, or any other word) |
Off | Off |
readonly |
On | Skipped and logged as [LGL] read-only: skipped ..., except for addresses in writable_address_ids. Stagemgr merges driven by LGL go to LGL Review unless both addresses are writable |
enabled |
On | On, for every address |
In development enabled is treated as readonly (with the allowlist), and
boot logs [LGL] sync 'enabled' downgraded to readonly in development, so a
copied config never writes to the live account from a developer's machine.
The test environment never syncs. The rules live in Lgl.configure_from,
called from each environment's after_initialize.
The LGL jobs run on the maintenance Resque queue (LglDonorImport runs on
import). In production script/resque-worker serves that queue from worker
3 only (default,maintenance), so one process makes every LGL request in turn; every other environment runs a¶
single worker on QUEUE=*. config/schedule.yml (and its .example) schedules four of them:
| Job | Schedule | Does |
|---|---|---|
LglPollConstituents |
every 3 hours | Reads constituents changed since the last poll and reconciles LGL merges |
LglSweepConstituents |
nightly at 03:10 | Drops LGL ID tags whose constituent no longer exists and re-syncs those customers |
LglDonorLevelsFetch |
nightly at 05:30 | Downloads the donor-tier saved report and queues LglDonorImport. Runs only when donor_tier_report_url is set, whatever sync is |
LglGiftHistoryWalk |
Sundays at 02:00 | Reads every linked customer's gifts from LGL (one request each, about 1.1 seconds apart to stay under LGL's 300 requests per 5 minutes, so roughly an hour) and refreshes their LGL Lapsed tag. Only reads LGL. Gives up after 25 consecutive LGL errors. Linking a customer and Re-sync with LGL refresh that one customer straight away |
After the first deploy with a key, run the backfill once to link every constituent whose External constituent ID already names a customer. It only writes local tags, never LGL:
bundle exec rake lgl:backfill # queue it on the maintenance queue
bundle exec rake lgl:backfill_now # or run it inline and print the counts
Writing Stagemgr ids into LGL¶
Constituents whose External constituent ID is blank, their own LGL id, or a
customer that no longer exists can be linked by email, once, by a developer.
A constituent is linked only when exactly one customer has one of its emails,
the same last name (ignoring case) and no other LGL ID; constituents with
several matches, none, or no email are only counted. Run the dry run first: it
reads LGL, prints the counts by kind and outcome, and writes nothing. Then run
the real one, which queues one sync per match on the maintenance queue:
bundle exec rake lgl:link_by_email_dry_run # counts only
bundle exec rake lgl:link_by_email # queue the links
It only sets External Constituent IDs in LGL (and the customer's LGL ID
tag). It never copies a name, street, phone or email in either direction:
each linked customer is marked as changed after the write, so the next poll
does not treat LGL as the newer copy.
Bringing existing constituents over¶
lgl:replicate makes every constituent with such an External constituent ID
a customer: it links it by email, creates a new customer, or leaves it in LGL
Review, as the poll does for new constituents. Run the dry run first; it prints
one line per constituent and writes nothing. Optional LGL ids limit either run
to those constituents and run it inline; without ids the real run is queued on
the maintenance queue:
bundle exec rake "lgl:replicate_dry_run[123,456]" # what it would do
bundle exec rake lgl:replicate # queue the run
With sync enabled it queues one sync per linked or created customer, three
seconds apart to stay under LGL's rate limit. With sync readonly the
customers are still created and linked locally, but no syncs are queued,
except an immediate one for each address in writable_address_ids: nothing
else could be written to LGL. The counts include how many were skipped, as
sync_skipped_readonly.
The first LglPollConstituents run after sync is enabled reads nothing: it
records the current time as its watermark and logs [LGL] poll: first run;
starting from <time>. Only LGL changes made after that moment are copied into
Stagemgr; earlier ones are not. A disabled install never starts the clock.
setup:doctor reports one LGL line naming the mode. LGL sync is disabled
(littlegreenlight.sync) and LGL sync is readonly — writes skipped for ...
(listing any writable ids, and saying when enabled was downgraded in
development) are both OK. It warns when sync is a word other than enabled,
readonly or disabled, and when sync is on but LGL_API_KEY is missing;
in enabled mode it makes one read to confirm the key and warns if LGL
rejects it.
docker compose¶
| Variable | Default | Purpose |
|---|---|---|
STAGEMGR_PORT |
8080 |
Host port for the app |
STAGEMGR_BIND |
127.0.0.1 |
Interface the published ports bind to |
MYSQL_PORT |
3306 |
|
REDIS_PORT |
6379 |
|
HOST_UID, HOST_GID |
-- | Linux only: renumber the container's app user so bind-mounted files come back owned by you |
DB_PREPARE_ON_BOOT |
true in development, false in production |
Run setup:bootstrap on container start. Accepts true/1/yes, any case |
RESQUE_QUEUES |
* |
Queues the containerized worker consumes |
First-run and CI¶
| Variable | Purpose |
|---|---|
ADMIN_EMAIL, ADMIN_PASSWORD |
Skip setup:admin's interactive prompts (scripted installs) |
MAIL_DUMP_DIR |
spec/mailers/theme_render_spec.rb writes every rendered email body under this directory, for before/after diffing |
Reports run from cron¶
db/reports/*.sh are plain shell + SQL, not Rails, and take their configuration
from the cron environment:
| Variable | Default | Purpose |
|---|---|---|
REPORT_EMAIL |
none -- the scripts abort without it | Recipient and envelope sender for mail_*.sh |
REPORT_SUBJECT_TAG |
StageMgr |
Prefix in the subject line, e.g. [StageMgr] Unfulfilled Flexpasses |
MY_CNF |
$HOME/.my.cnf |
MySQL defaults file the HUD count scripts (db/reports/hud/*.sh) authenticate with |
config/server.yml¶
Copied from config/server.yml.example. Keys under all: apply to every
environment; an environment block (development:, test:, production:)
overrides them by deep merge.
Identity and mount point¶
| Key | Meaning |
|---|---|
app_name |
Display name in page titles and as the fallback house name before a theater row exists |
host |
Host used to build absolute URLs (email links, calendar feeds) |
host_protocol |
http or https for those URLs |
sub_uri |
Mount point when the app is not served from /, e.g. /tickets. Passed to ActionMailer as :script_name, so mail sent from a worker gets the prefix exactly once. Leave it blank on a standalone install |
secure_root_url |
Absolute base for the links in the generated public calendar feed (ResqueJobs::GenerateCalendar) |
static_cache_dir |
Where that generated calendar is written |
ext_site_wrapper |
Layout for the public order pages. standalone for a self-contained install; the name of an externally supplied layout when a marketing site wraps the app |
site_theme |
Directory under sites/ holding this house's editorial copy. Commented out = generic copy (Theming) |
Sales rules¶
| Key | Meaning |
|---|---|
order_expiration_in_minutes |
How long an unpaid order is held |
restrict_sales_due_to_time_at_minutes_before |
Stop public sales this many minutes before curtain (inventory routes to the box office) |
minutes_before_performance_close_to_third_party_sales |
The same cut-off for theater users |
restrict_sales_due_to_capacity_at |
Seats remaining at or below which self-service sales stop; also drives "Call box office" on the calendar |
max_ticket_dropdown |
Largest quantity a patron can pick in one order |
resourced_default_runtime_minutes |
Assumed running time when productions.running_time is blank |
calendar_display.warning_at, calendar_display.critical_at |
Calendar heatmap thresholds, as a percentage of seats remaining |
allowed_order_task_suppressions |
Which order tasks a payment type may suppress |
Reports and storage¶
| Key | Meaning |
|---|---|
hud_export_directory |
Where the HUD export jobs write their text files |
archive_directory |
Destination for data-retention archives. Blank disables the archive/prune pipeline -- the jobs refuse to run rather than delete rows nothing exported. Must be covered by your backups |
max_report_date_range_days |
Widest date range a report may request |
report_timeout_seconds |
Report query timeout |
report_frequent_customer_at, report_frequent_customer_range_days |
"Frequent customer" threshold on the house management report |
theater: -- facts about the house¶
Plain, non-editorial facts printed on public order pages and in email. Every one
may be left blank: the sentence that would have used it is omitted rather than
printed empty. Anything with a voice to it belongs in a
site theme instead. The full key list, with fallbacks, is in
Theming → the theater: block.
The house's name is not a key here
It is the name of the Default theater row in the database, so the one
proper noun that appears in dozens of places has a single source the box
office already maintains. Renaming that row renames the house everywhere.
my_emma:¶
| Key | Meaning |
|---|---|
newsletter_group, coupon_group |
Names of the MyEmma groups a patron joins when they tick the mailing-list box |
create_production_groups, create_theater_groups |
Whether new productions/theaters get their own Emma groups |
The two group-name keys have three distinct states:
| State | Behaviour |
|---|---|
Key absent from server.yml |
The historical names Newsletter and Flash Offers, so a config predating these keys keeps behaving as it did |
| Key present but blank | Nobody is added to that group -- an explicit opt-out, not a lookup for "" |
| Key present with a name | That group |
Group ids are resolved by name and cached for five minutes: the API resolves a
name by listing every group in the account, so a lookup per order is expensive --
but a permanent memo would go on serving the id of a group that has since been
renamed. When MyEmma is configured and writable, setup:doctor checks that
each configured name actually exists in the account and warns if it does not.
email:¶
| Key | Meaning |
|---|---|
delivery_method |
postmark, file (writes to tmp/mails -- the development default, so a fresh install needs no mail credentials), sendmail, test |
addresses.box_office |
Default From: for patron mail, and TheaterInfo#box_office_email; also a recipient of the refunded-fulfilled-order alert |
addresses.flex_pass_notifications |
Flex pass activity |
addresses.membership_notifications |
Membership activity |
addresses.supervisor_notifications |
High-priority system notices, including the refunded-fulfilled-order alert (NotificationMailer#refunded_fulfilled_item_alert, queued as a NotificationTask by Order#refund!) |
addresses.wheelchair_conversion_notifications |
Seat accessibility conversions |
addresses.software_address |
System/administrative mail; the last-resort sender |
addresses.exception_notifications |
Crash reports from the exception notifier |
exception_notifications falls back to software_address. With neither
configured the exception-notification middleware is left out of the stack
entirely and a warning is logged: EmailNotifier would otherwise build a mail
with no recipients and raise from inside the middleware while it was handling
the real exception, replacing a useful 500 page with a confusing one.
payment_processing:¶
| Key | Meaning |
|---|---|
default_gateway |
stripe, paypal, bogus (the development/test default) |
default_recurring_gateway |
Gateway for subscriptions |
additional_card_types |
Comma-separated extra card brands to accept |
test_credit_card, test_card_brand |
Values the non-production forms pre-fill |
RequiredSecrets reads these: a stripe gateway makes STRIPE_SECRET_KEY
mandatory in production, and delivery_method: postmark makes
POSTMARK_API_TOKEN mandatory. See Credentials.
The Resque dashboard¶
config/routes.rb mounts the Resque web UI at /admin/resque with no other
authentication in front of it. Its HTTP basic-auth password comes from
RESQUE_ADMIN_PASSWORD or the resque_admin_password credential:
| Situation | Result |
|---|---|
| A password is configured | Basic auth, compared with secure_compare |
| None, in production | Denied to everyone (fail closed). An open queue dashboard exposes job arguments -- order ids, email addresses -- and offers a one-click "Clear failed jobs" |
| None, outside production | Open to anyone who can reach it |
A resque_admin_password: still sitting in config/server.yml keeps working
but warns once at boot. It is the only key with that plaintext fallback left.
The test environment reads the example file¶
config/environments/test.rb loads the tracked config/server.yml.example,
not the config/server.yml you copied it to, so every checkout and every CI run
tests against the same values instead of whatever a developer's gitignored copy
happens to say. Cucumber forces RAILS_ENV=test too, so it does the same.
That makes the test: block part of the test suite. Its values are deliberate
sentinels -- 555-BOX-OFFICE, PICKUPWINDOW.TEST, https://WEBSITE.TEST,
Test Theater Box Office, Test Director -- obviously fake, so a spec cannot
pass against a developer's real config by accident. Specs and Cucumber features
assert them.
Adding a key the tests need
Add it to the test: block of config/server.yml.example, not to your own
config/server.yml. test.rb also forces ext_site_wrapper to
standalone regardless of what the file says.
Keys that are gone¶
| Key | Why |
|---|---|
filestore_hash (server.yml) |
Read by nothing. Deleted from all three blocks of the example |
email.addresses.online_errors (server.yml) |
Added in 2011 and never read by any code; unhandled exceptions go to exception_notifications. Deleted from the example -- a value left in your own file is ignored |
root_url (server.yml) |
Read by nothing -- only secure_root_url is. Still present in the example's environment blocks; do not rely on it |
resque_admin_password (server.yml) |
Now a deprecated fallback that warns at boot; use RESQUE_ADMIN_PASSWORD or the credential |
STRIPE_PUBLISHABLE_KEY |
Read by nothing; never prompted for |
config.external_site_root (development.rb) |
Pointed at a developer's home directory and was read by nothing |