Customization Guide
This guide covers the supported no-code customization surface for forks of The Pool as it exists now.
The goal is to let forks rebrand, restyle, and reconfigure the platform through config, while keeping checkout, reports, emails, and the Worker aligned.
The structured config model in _config.yml is now the canonical fork-facing surface.
Start Here
For most forks, the main customization files are:
Use ./scripts/dev.sh --podman for local verification after config changes.
For normal operator edits, use the private admin dashboard at /admin/ or /es/admin/:
- Settings edits platform-wide configuration and runtime admin tools for super admins.
- Add-ons edits platform add-on products for super admins.
- Campaigns edits campaign settings, page content, tiers, support items, campaign add-ons, stretch goals, ongoing items, diary entries, and decisions.
- Settings -> Users is runtime-only and saves directly to Worker KV at
admin-users:v1; it does not publish to GitHub. - Secrets & credentials is read-only status. Secret values still belong in Worker secrets, GitHub repository secrets, or ignored local env files.
Treat _config.local.yml as an override-only file. Keep canonical fork settings in _config.yml, and use the local file only for things that should differ on your machine, like localhost URLs or local-only campaign visibility.
The normal local path is now localhost-based:
- site:
http://127.0.0.1:4000 - Worker:
http://127.0.0.1:8787
The generated static site also now excludes repo-internal folders like worker/, scripts/, and tests/, so static verification more closely matches what a fork would actually publish.
Supported Config Areas
The site config is organized around these fork-facing sections:
- top-level
title/description seoplatformpricingtaxshippingreportsi18ndesigndebugperformancelaunch_remindersadd_onscheckoutcache
Top-level title / description
Use the top-level Jekyll metadata for the site’s default search/social identity.
Supported keys:
titledescription
These values feed:
- default HTML
<title>fallback - default meta description fallback
- site-wide
WebSiteJSON-LD fallback description
platform.name is still the primary visible brand surface. Treat top-level title / description as the fork-facing SEO baseline rather than the main UI-branding interface.
platform
Use platform for identity, URLs, and brand assets.
Supported keys:
nameversionrelease_labelcompany_namesupport_emailpledges_email_fromupdates_email_fromsite_urlworker_urldefault_creator_nametimezonelogo_pathfooter_logo_pathfavicon_pathdefault_social_image_path
These values feed:
- header / footer branding
- release metadata for docs/public copy when a fork wants to surface its current milestone
- page titles and meta tags
- app-title metadata for mobile/share surfaces
- default social-card image
- campaign creator fallback copy
- campaign deadline/countdown behavior and Worker scheduled lifecycle windows
- checkout / Manage Pledge UI copy and bootstrapped client config
- Worker email branding when mirrored
Notes:
platform.*is the primary branding surface.platform.versionshould be the canonical machine-readable product version for the site, whileplatform.release_labelcan stay friendlier for public-facing copy such asv1.0.3.platform.timezonemust be a supported IANA timezone. It defaults toAmerica/Denverso existing forks keep the old lifecycle behavior until they change it.- top-level
title/authorstill exist in Jekyll, but treat them as general site metadata / fallback rather than the main fork-customization interface. platform.default_social_image_pathis the supported default for OG/Twitter cards when a page or campaign does not provide a more specific image.platform.logo_pathis also the mirrored brand mark used in supporter emails.- The domain in
platform.pledges_email_fromandplatform.updates_email_frommust be authorized by the configured email provider. With Resend, authorizingpool.example.comdoes not authorizeexample.com, and vice versa.
Example:
platform:
name: My Fork
version: 1.0.3
release_label: v1.0.3
company_name: Example Studio
support_email: [email protected]
pledges_email_from: "My Fork <[email protected]>"
updates_email_from: "My Fork <[email protected]>"
site_url: https://crowdfund.example.com
worker_url: https://pledge.example.com
default_creator_name: Example Studio
timezone: America/Denver
logo_path: /assets/images/brand/logo-square.png
footer_logo_path: /assets/images/brand/logo-footer.png
favicon_path: /assets/images/brand/favicon.png
default_social_image_path: /assets/images/brand/social-card.png
pricing
Use pricing for tax and platform-tip defaults that must stay consistent across the site and Worker. Shipping fallback fees live under shipping.
Supported keys:
sales_tax_ratedefault_tip_percentmax_tip_percent
Example:
pricing:
sales_tax_rate: 0.0825
default_tip_percent: 5
max_tip_percent: 15
tax
Use tax for the Worker-side tax engine selection and its non-secret lookup settings.
Supported keys:
providerorigin_countryuse_regional_originnm_grt_api_basezip_tax_api_base
Current provider values:
flatkeeps the legacy configuredpricing.sales_tax_rateoffline_rulesuses vendored rules for international VAT/GST and state-level fallback handlingnm_grtuses a vendored New Mexico starter dataset and can refine full NM street-address lookups against the free EDAC GRT APIzip_taxuses ZIP.TAX for local / jurisdiction-level US tax lookups and falls back tooffline_rulesfor non-US/CA destinations
Current UX note:
- cart and checkout can display provisional tax as
--until the browser has enough destination detail for the configured provider nm_grtis currently the most complete built-in local-data path for US jurisdictional tax and generally needs full New Mexico street-level destination data before it can return a precise result
Example:
tax:
provider: nm_grt
origin_country: US
use_regional_origin: false
nm_grt_api_base: https://grt.edacnm.org
zip_tax_api_base: https://api.zip-tax.com
If you enable zip_tax, also set the Worker secret ZIP_TAX_API_KEY. Keep that secret out of _config.yml.
The vendored New Mexico starter file lives in worker/src/tax-data/nm-grt-starter.js. Refresh it with:
node ./scripts/update-nm-grt-starter.mjs
i18n
Use i18n for the supported locale model on the static site.
Supported keys:
default_langsupported_langslanguage_labelspages
pages is the public-page route map used by the shared locale helpers. It lets forks add a new language by config plus translated content instead of editing navigation logic by hand.
Example:
i18n:
default_lang: en
supported_langs:
- en
- es
language_labels:
en: English
es: Español
pages:
home:
en: /
es: /es/
about:
en: /about/
es: /es/about/
terms:
en: /terms/
es: /es/terms/
Current supported pattern:
- shared UI/system copy lives in
_data/i18n/{lang}.yml - non-default public pages live under a locale prefix like
/es/ - shared runtime/browser messages are emitted through
assets/i18n.json - Worker supporter emails reuse that shared locale catalog plus persisted
preferredLang - campaign chrome such as the hero video button/loading text, supporter-community teaser copy, diary tabs, production-phase controls, gallery accessibility labels, cart-button summaries, and checkout tax-location helper copy also now comes from
_data/i18n/{lang}.yml - the shared footer language switcher is automatic when more than one language is configured
- long-form pages such as
aboutandtermsshould use localized source pages rather than trying to store every paragraph in YAML - public metadata and structured-data language hints also follow the same locale model, so localized public pages do not need a second SEO-only translation system
What this means in practice:
- changing
i18n.default_langonly changes the default locale the site resolves against - adding a new
_data/i18n/{lang}.ymlfile is enough for shared chrome, runtime UI, and Worker supporter-email copy - it is not enough for a fully translated site by itself
- full language support also needs:
- the new language added to
i18n.supported_langs - its label added to
i18n.language_labels - localized routes added to
i18n.pages - localized source pages for long-form content you actually want translated
- the new language added to
- tokenized pledge-management routes keep working across locales because the shared language switcher preserves the current query string and hash
Recommended fork workflow:
- Copy
_data/i18n/en.ymlto_data/i18n/{lang}.yml - Add the language to the
i18nblock in_config.yml - Add localized source pages for long-form routes such as
/about/and/terms/ - Build locally and verify both the shared UI copy and the localized routes
SEO surface
Current SEO fundamentals are intentionally bounded. Forks should treat these as the supported knobs:
- top-level
title - top-level
description seo.x_handleseo.same_asseo.index_public_community_hubseo.default_social_image_altseo.og_locale_overridesplatform.nameplatform.site_urlplatform.default_social_image_path- localized page
title/descriptionfront matter on public pages - campaign
title,short_blurb, and hero images
That surface currently controls:
- canonical URLs
- meta descriptions
- Open Graph and Twitter previews
- sitemap URL generation
- site-wide
Organization/WebSiteJSON-LD - campaign
CreativeWork/ breadcrumb JSON-LD - fallback social-image alt text
- Open Graph locale strings
The implementation is deliberately narrow:
- private/tokenized/supporter-only flows are marked
noindex robots.txtandsitemap.xmlonly advertise the public surface- there is no giant per-page SEO settings matrix beyond the content fields the site already supports
Example:
seo:
x_handle: dustwave
same_as:
- https://www.instagram.com/dustwave
- https://www.youtube.com/@dustwave
index_public_community_hub: true
default_social_image_alt: "Social card for your deployment"
og_locale_overrides:
en: en_US
es: es_ES
debug
Use debug for shared browser-runtime and Worker console logging.
Supported keys:
console_logging_enabledverbose_console_logging
What they do:
console_logging_enabled: falsesuppresses browser and Workerconsoleoutput across the shared cart, campaign, community, live-stats, Manage Pledge, webhook, admin, and scheduled-task runtimesverbose_console_logging: falsekeeps the logger active but suppresses lower-severity debug/info/log noise while still allowing warnings and errors
These defaults are intentionally true in _config.yml, so forks start with full diagnostics available and can turn logging down later without code changes.
When enabled, the shared loggers now emit:
- ISO timestamps
- consistent browser / Worker scope prefixes
- severity labels like
LOG,WARN, andERROR - normalized
Errorpayloads - browser capture for uncaught errors and unhandled promise rejections
shipping
Use shipping for origin and fallback shipping settings plus the preset catalog for common physical goods.
Supported keys today:
origin_ziporigin_countryfallback_flat_ratefree_shipping_defaultdefault_optionusps.enabledusps.client_idusps.api_baseusps.timeout_msusps.quote_cache_ttl_secondsusps.failure_cooldown_secondsusps.rate_limit_cooldown_secondspresets
Campaigns can also optionally set shipping_fallback_flat_rate in front matter. When present, that campaign-specific fallback overrides the global shipping.fallback_flat_rate if USPS quoting is unavailable.
Campaigns can also optionally set shipping_options in front matter to opt into the limited backer-facing shipping policy set:
signature_requiredadult_signature_required
standard is always available implicitly and does not need to be listed.
When a pledge qualifies for multiple delivery options, the shared cart and Manage Pledge UIs render the same localized selector and the Worker persists the selected option as part of the canonical shipping total.
Important secret boundary:
- keep
shipping.usps.client_idin_config.yml - keep the companion
USPS_CLIENT_SECRETin Worker secrets orworker/.dev.vars - do not commit the secret into Jekyll config
The checkout destination list is intentionally separate from those knobs now. Maintain the currently allowed shipping countries in _data/shipping_countries.yml instead of editing browser runtime code.
Example:
shipping:
origin_zip: "87120"
origin_country: "US"
fallback_flat_rate: 3.00
free_shipping_default: false
default_option: standard
usps:
enabled: true
client_id: "your-usps-client-id"
api_base: ""
timeout_ms: 5000
quote_cache_ttl_seconds: 600
failure_cooldown_seconds: 300
rate_limit_cooldown_seconds: 1800
presets:
poster:
weight_oz: 5
packaging_weight_oz: 3
length_in: 18
width_in: 3
height_in: 3
stack_height_in: 0.5
vinyl:
weight_oz: 18
packaging_weight_oz: 4
length_in: 13
width_in: 13
height_in: 1
What this enables:
- a deployment-level USPS shipping origin
- a deployment-level free-shipping default that campaigns can still override
- a configured fallback rate if live carrier quoting is unavailable
- a fork-facing USPS quote policy surface for timeouts, short-lived quote reuse, and temporary cooldowns after repeated failures or rate limiting
- a shared delivery-option selector surface in cart and Manage Pledge without opening up arbitrary carrier-speed choices
- reusable
shipping_presetnames in campaign tiers so forks do not need to repeat common merch dimensions - optional preset-level USPS profile hints for item types that need a different domestic quote shape
- optional preset-level domestic mail-class ordering for products that qualify for cheaper USPS classes like Media Mail
Preset and override metadata can include:
weight_ozpackaging_weight_ozlength_inwidth_inheight_instack_height_inmanual_domestic_rateusps_domestic.processing_categoryusps_domestic.rate_indicatorusps_domestic.destination_entry_facility_typeusps_domestic.price_typeusps_domestic.mail_classes
weight_oz is the item weight. packaging_weight_oz is a one-time packing allowance for that line item, and stack_height_in lets multi-quantity physical tiers stack more realistically than simple height * qty.
The safest pattern is to encode a deliberate cheapest-valid order per preset instead of trying to infer “letter” or “flat” eligibility from raw dimensions at runtime. The current site now uses:
stickermanual_domestic_rate: FIRST_CLASS_FLAT- then a cheaper single-piece domestic USPS profile if the shipment no longer qualifies for flats
signed_scriptmanual_domestic_rate: FIRST_CLASS_FLAT- then
MEDIA_MAIL - then
USPS_GROUND_ADVANTAGE - then
PRIORITY_MAIL
cd,dvd,blurayMEDIA_MAILUSPS_GROUND_ADVANTAGEPRIORITY_MAIL
If a product does not reliably qualify for a cheaper class, leave it on the default parcel path. Also note that the current USPS Prices API path does not expose domestic First-Class letter/flat rating directly, so “large envelope” logic is implemented here as an explicit manual table (FIRST_CLASS_FLAT), not as a live USPS API quote.
add_ons
Use add_ons for a global, platform-level merch or upsell catalog that is not tied to a single campaign’s support_items.
The current Worker path treats these as bundle-level selections. Pending checkout manifests can also store an anchor campaign so multi-campaign carts remain supported while later settlement and management flows stay campaign-compatible.
Supported keys today:
enabledlow_stock_thresholdproducts
Each product currently supports:
idnamedescriptionimage_urlpricecategoryinventoryshipping_presetshippingsource_urlvariant_option_namevariants
Example:
add_ons:
enabled: true
low_stock_threshold: 5
products:
- id: dust-wave-tshirt
name: "DUST WAVE T-Shirt"
description: "Our official t-shirt. 100% cotton."
price: 25.00
category: physical
shipping_preset: tshirt
source_url: "https://shop.example.com/"
variant_option_name: Size
variants:
- { id: xs, label: XS, inventory: 1 }
- { id: s, label: S, inventory: 2 }
- { id: m, label: M, inventory: 4 }
This is meant for fixed-price catalog items and simple variants like shirt sizes. It is separate from campaign support_items, which remain campaign-scoped and amount-based.
In the admin dashboard, platform add-ons live in the top-level Add-ons tab, while campaign-scoped add-ons live in the owning campaign’s Add-Ons subtab. Legacy IDs are preserved. New product IDs derive from the product name, and new variant IDs derive from the variant label, so editors do not need to type slug values by hand.
Add-on shipping behavior:
category: digitalmeans the add-on never contributes to shippingcategory: physicalmeans the add-on participates in the same shipping calculator used for physical tiers and physical support items- physical add-ons can either:
- reference a shared
shipping_preset - or provide explicit
shipping.weight_oz,shipping.packaging_weight_oz,shipping.length_in,shipping.width_in,shipping.height_in, andshipping.stack_height_in
- reference a shared
- in the dashboard, those explicit weight/dimension fields appear only for physical add-ons when the shipping preset is
none
Current add-on inventory behavior:
inventorycan live on the product itself or on each variantlow_stock_thresholdcontrols when the shared cart/manage UI shows scarcity messaging- sold-out variants are removed from the shared product-state surface unless the supporter already owns that exact variant on an existing pledge
- saved add-on sold counts live in
add-on-inventory-sold:v1after the first projection bootstrap, and pledge create, modify, and cancel paths keep the projection current - the cart and Manage Pledge both use the same shared add-on product-card model, so forks do not need to style or configure two different merch systems
- the add-on section heading and support note are localized through the normal runtime i18n files, and the support note interpolates the configured site author name automatically
Campaigns can also define campaign-scoped add-ons directly in campaign front matter under campaign_add_ons.
That campaign-owned catalog uses the same product shape as the global add_ons.products entries, but behaves differently in two important ways:
- campaign add-ons render under a separate
Campaign Add-onssection in cart and Manage Pledge - campaign add-ons count toward the owning campaign subtotal / funding progress and follow that campaign’s shipping rules
By contrast, global add_ons.products remain platform merch:
- they render under the normal
Add-onssection - they do not count toward campaign funding totals
- physical global add-ons combine into one separate platform shipment / shipping charge
reports
Use reports for campaign-runner report behavior that must stay aligned with Worker scheduling and dashboard report generation.
Supported keys today:
campaign_runner.enabledcampaign_runner.daily_pledge_report_enabledcampaign_runner.fulfillment_report_enabledcampaign_runner.send_hourcampaign_runner.send_minutecampaign_runner.include_stats_summarycampaign_runner.include_csv_attachmentcampaign_runner.email_subject_prefix
Current behavior:
- campaign-level recipients live in campaign front matter as
runner_report_emails - if that campaign field is missing or empty, no campaign-runner emails are sent for that campaign
- the send window is interpreted in
platform.timezoneso report timing stays aligned with the rest of the campaign lifecycle model email_subject_prefixcan be set to an empty string to disable the prefix entirely- when the prefix is omitted at runtime, the Worker falls back to
[platform.name] - report subjects stay concise and deliverability-oriented: no emoji, short report labels, and a consistent prefix + report-kind + campaign-title pattern
- daily pledge emails use a campaign-only summary with total pledges, new pledges in the previous 24 hours, pledged total, goal progress, and deadline countdown/passed time
- fulfillment sends are split by fulfiller:
- campaign-runner recipients receive only the campaign-fulfilled rows
platform.support_emailreceives a separate platform-fulfillment email when platform add-on rows exist
- fulfillment summaries are intentionally concise and fulfillment-oriented; they do not reuse the daily pledge-report body summary
- both report types can include a short guidance note in the body so runners get campaign-stage-specific encouragement or fulfillment communication reminders alongside the CSV
Example:
reports:
campaign_runner:
enabled: true
daily_pledge_report_enabled: true
fulfillment_report_enabled: true
send_hour: 7
send_minute: 0
include_stats_summary: true
include_csv_attachment: true
email_subject_prefix: "[My Fork]"
What this enables:
- daily campaign-scoped pledge-ledger emails during live campaigns
- one-time fulfillment exports after a campaign deadline passes
- separate campaign-runner and platform fulfillment emails when both campaign and platform items need delivery
- optional body summaries and optional CSV attachments without changing campaign content files
- a consistent subject prefix, which defaults to
"[The Pool]"in this repo and falls back to[platform.name]if omitted at runtime
Per-campaign recipient example:
runner_report_emails:
- [email protected]
- [email protected]
design
Use design for curated design-system overrides that do not require Sass edits.
These values are emitted into the generated stylesheet assets/main.css, which keeps the design-variable bridge compatible with the site’s strict CSP. assets/theme-vars.css remains as a compatibility artifact, but public layouts do not request it separately. Forks do not need to edit Sass just to change supported tokens.
The same generated CSS variables also now theme the on-site Stripe Elements sidecar, so supported typography/color/radius overrides carry through the custom checkout payment UI without adding a separate checkout-only config layer.
A deliberately smaller subset of the same branding surface is mirrored into the Worker so supporter emails can reuse the configured logo, font stacks, primary color, border/surface colors, and button radius.
Current supported keys:
- typography:
font_bodyfont_display
- layout:
layout_max_width
- radius:
radius_smradius_chipradius_mdradius_lgradius_xl
- text:
color_textcolor_text_strongcolor_text_mutedcolor_text_soft
- surfaces:
color_page_backgroundcolor_surface_basecolor_surface_subtlecolor_surface_softcolor_surface_strongcolor_page_background_overlaycolor_surface_base_overlaycolor_surface_subtle_overlay
- borders:
color_bordercolor_border_strongcolor_border_soft
- primary / emphasis:
color_primarycolor_primary_softcolor_primary_bordercolor_primary_hovercolor_primary_focus_ringcolor_progress
- feedback / tints:
color_successcolor_danger_softcolor_danger_softersurface_tint_softersurface_tint_softsurface_tint_mediumsurface_tint_hoversurface_tint_strong
Example:
design:
font_body: '"Source Sans 3", sans-serif'
font_display: '"Space Grotesk", sans-serif'
layout_max_width: 1080px
radius_md: 12px
radius_xl: 18px
color_text: "#1f2430"
color_page_background: "#f6f3ee"
color_surface_base: "#ffffff"
color_border: "#d9d2c7"
color_primary: "#111111"
color_primary_hover: "#000000"
color_progress: "#111111"
checkout
The checkout section is intentionally narrow.
Supported key today:
stripe_publishable_key
The first-party cart runtime and on-site custom checkout flow are treated as built-in platform behavior, not as fork-facing mode switches.
cache
Use cache to tune public live-read browser caching.
Supported keys:
live_stats_ttl_secondslive_inventory_ttl_seconds
performance
Use performance for public-page performance knobs that a fork may need to tune without code changes.
Supported keys:
intent_prefetch_enabledintent_prefetch_delay_msintent_prefetch_limit
These control the safe same-origin document prefetch runtime loaded on public pages. The default is enabled, with conservative route/query exclusions and a low per-page limit. Private app surfaces such as admin, checkout, Manage Pledge, and supporter-community routes do not load the public prefetch runtime.
Super admins can edit these fields in the dashboard under Settings -> Advanced performance. Published changes update _config.yml, mirror the Worker-facing INTENT_PREFETCH_* values, and take effect on static pages after the normal rebuild/deploy path.
launch_reminders
Use launch_reminders for the public one-time reminder form shown on upcoming campaign pages.
Supported keys:
enabledturnstile_site_key
Current behavior:
- the form only renders for campaigns whose effective state is
upcoming - the public site key can be blanked in
_config.local.ymlto hide the widget locally - the matching secret belongs in Worker secrets as
TURNSTILE_SECRET_KEYorLAUNCH_REMINDER_TURNSTILE_SECRET_KEY - reminder signup, unsubscribe, and dispatch logic lives in the Worker and reuses the existing Resend email module
Example:
launch_reminders:
enabled: true
turnstile_site_key: "0x..."
Site-Only vs Worker-Mirrored Settings
Some settings only affect the Jekyll build and browser-owned UI. Others are also reflected into the Worker env automatically.
Not Mirrored By The Sync Script
These can be changed in _config.yml without adding new Worker env entries:
i18n.*platform.versionplatform.release_labeladmin.production_site_urladmin.production_worker_urlshipping.presetsadd_ons.*- campaign front matter, including
campaign_add_ons, content blocks, diary entries, tiers, support items, stretch goals, and decisions - design/layout tokens that are only consumed by the generated site CSS and not by supporter emails
These values still matter to the generated site and, in some cases, to Worker-fetched static API payloads. They are simply not written into worker/wrangler.toml by scripts/sync-worker-config.rb.
Auto-Mirrored To Worker
These site-config values are also reflected into the Worker env values in worker/wrangler.toml:
title->SITE_TITLEdescription->SITE_DESCRIPTIONauthor->PLATFORM_AUTHORplatform.name->PLATFORM_NAMEplatform.company_name->PLATFORM_COMPANY_NAMEplatform.default_creator_name->PLATFORM_DEFAULT_CREATOR_NAMEplatform.support_email->SUPPORT_EMAILplatform.pledges_email_from->PLEDGES_EMAIL_FROMplatform.updates_email_from->UPDATES_EMAIL_FROMplatform.logo_path->EMAIL_LOGO_PATHplatform.footer_logo_path->PLATFORM_FOOTER_LOGO_PATHplatform.favicon_path->PLATFORM_FAVICON_PATHplatform.default_social_image_path->PLATFORM_DEFAULT_SOCIAL_IMAGE_PATHplatform.site_url->SITE_BASEandCORS_ALLOWED_ORIGINplatform.worker_url->WORKER_BASEseo.default_social_image_alt->SEO_DEFAULT_SOCIAL_IMAGE_ALTseo.x_handle->SEO_X_HANDLEseo.same_as->SEO_SAME_ASseo.index_public_community_hub->SEO_INDEX_PUBLIC_COMMUNITY_HUBadmin.users->ADMIN_USERS_JSONadmin.local_test_campaigns->ADMIN_TEST_CAMPAIGNSin the dev envcheckout.stripe_publishable_key->STRIPE_PUBLISHABLE_KEYdesign.font_body->EMAIL_FONT_FAMILYdesign.font_display->EMAIL_HEADING_FONT_FAMILYdesign.color_text->EMAIL_COLOR_TEXTdesign.color_text_muted->EMAIL_COLOR_MUTEDdesign.color_surface_subtle->EMAIL_COLOR_SURFACEdesign.color_border->EMAIL_COLOR_BORDERdesign.color_primary->EMAIL_COLOR_PRIMARYdesign.radius_lg->EMAIL_BUTTON_RADIUSpricing.sales_tax_rate->SALES_TAX_RATEtax.provider->TAX_PROVIDERtax.origin_country->TAX_ORIGIN_COUNTRYtax.use_regional_origin->TAX_USE_REGIONAL_ORIGINtax.nm_grt_api_base->NM_GRT_API_BASEtax.zip_tax_api_base->ZIP_TAX_API_BASEpricing.flat_shipping_rate->FLAT_SHIPPING_RATE(legacy compatibility only; prefershipping.fallback_flat_rate)pricing.default_tip_percent->DEFAULT_PLATFORM_TIP_PERCENTpricing.max_tip_percent->MAX_PLATFORM_TIP_PERCENTshipping.origin_zip->SHIPPING_ORIGIN_ZIPshipping.origin_country->SHIPPING_ORIGIN_COUNTRYshipping.fallback_flat_rate->SHIPPING_FALLBACK_FLAT_RATEshipping.free_shipping_default->FREE_SHIPPING_DEFAULTshipping.default_option->SHIPPING_DEFAULT_OPTIONshipping.usps.enabled->USPS_ENABLEDshipping.usps.client_id->USPS_CLIENT_IDshipping.usps.api_base->USPS_API_BASEshipping.usps.timeout_ms->USPS_TIMEOUT_MSshipping.usps.quote_cache_ttl_seconds->USPS_QUOTE_CACHE_TTL_SECONDSshipping.usps.failure_cooldown_seconds->USPS_FAILURE_COOLDOWN_SECONDSshipping.usps.rate_limit_cooldown_seconds->USPS_RATE_LIMIT_COOLDOWN_SECONDSplatform.timezone->PLATFORM_TIMEZONEreports.campaign_runner.enabled->CAMPAIGN_RUNNER_REPORTS_ENABLEDreports.campaign_runner.daily_pledge_report_enabled->CAMPAIGN_RUNNER_DAILY_PLEDGE_REPORT_ENABLEDreports.campaign_runner.fulfillment_report_enabled->CAMPAIGN_RUNNER_FULFILLMENT_REPORT_ENABLEDreports.campaign_runner.send_hour->CAMPAIGN_RUNNER_REPORT_HOURreports.campaign_runner.send_minute->CAMPAIGN_RUNNER_REPORT_MINUTEreports.campaign_runner.include_stats_summary->CAMPAIGN_RUNNER_INCLUDE_STATS_SUMMARYreports.campaign_runner.include_csv_attachment->CAMPAIGN_RUNNER_INCLUDE_CSV_ATTACHMENTreports.campaign_runner.email_subject_prefix->CAMPAIGN_RUNNER_EMAIL_SUBJECT_PREFIXlaunch_reminders.enabled->LAUNCH_REMINDERS_ENABLEDdebug.console_logging_enabled->DEBUG_CONSOLE_LOGGING_ENABLEDdebug.verbose_console_logging->DEBUG_VERBOSE_CONSOLE_LOGGINGperformance.intent_prefetch_enabled->INTENT_PREFETCH_ENABLEDperformance.intent_prefetch_delay_ms->INTENT_PREFETCH_DELAY_MSperformance.intent_prefetch_limit->INTENT_PREFETCH_LIMITcache.live_stats_ttl_seconds->LIVE_STATS_CACHE_TTL_SECONDScache.live_inventory_ttl_seconds->LIVE_INVENTORY_CACHE_TTL_SECONDS
Local bootstrap super-admin emails are intentionally not mirrored from _config.yml. Put them in ignored worker/.dev.vars as ADMIN_BOOTSTRAP_EMAILS; npm run secrets:dev seeds that value from worker/.dev.vars.example when missing.
The sync script also writes derived URL values:
- production
[vars]getsCANONICAL_SITE_BASE/CANONICAL_WORKER_BASEfrom the productionplatform.site_url/platform.worker_url - dev
[env.dev.vars]gets localSITE_BASE/WORKER_BASEfrom_config.local.yml, whileCANONICAL_SITE_BASE/CANONICAL_WORKER_BASEstay pinned to the production values from_config.yml
The browser dashboard Reports tab previews and downloads pledge/fulfillment CSVs for campaigns the admin can access. It does not send report emails and does not write “sent” markers.
The repo keeps those values aligned automatically through the main local/dev/test paths. After changing them, restart the local stack so the site and Worker both pick up the new values:
./scripts/dev.sh --podman
For convenience, the repo now includes:
npm run sync:worker-config
That command syncs the Worker-mirrored values in worker/wrangler.toml from _config.yml and _config.local.yml.
It does not write Worker secrets, media files, or generated optimization outputs. USPS OAuth secrets, Stripe secret keys, Resend keys, ZIP.TAX keys, Turnstile secrets, GitHub tokens, and Cloudflare deploy credentials still belong in Worker secrets, GitHub repository secrets, or ignored local env files.
Launch reminders have one public setting and one secret boundary:
_config.ymllaunch_reminders.enabledcontrols whether upcoming campaign pages render the reminder form._config.ymllaunch_reminders.turnstile_site_keyis the public Cloudflare Turnstile site key used by that form._config.local.ymlmay overridelaunch_reminders.turnstile_site_keywith an empty string to hide the widget in local development, matching the local admin sign-in Turnstile override.- The matching Turnstile secret belongs in Worker secrets as
TURNSTILE_SECRET_KEYorLAUNCH_REMINDER_TURNSTILE_SECRET_KEY. - Reminder emails use the existing Resend-backed Worker email module and the configured
platform.updates_email_fromsender.
Dashboard-uploaded media also does not add new sync-script config. Uploads commit source files into the existing asset directories; image/video uploads request the Optimize dashboard media workflow after the commit succeeds. npm run media:optimize / npm run media:optimize:check, the Podman-backed variants for machines without native optimizers, and the same workflow handle image compression, responsive WebP variants at 320w, 480w, 640w, 960w, and 1600w, and WebM derivatives outside the Worker.
Generated CSS/JS minification is also outside the Worker and dashboard save path. Production deploys run npm run assets:minify only after Jekyll writes _site, so forks should keep source assets readable in assets/ and let the deploy artifact step handle minified output. Cloudflare edge compression should stay enabled, but Cloudflare Auto Minify should stay disabled to avoid a second rewriting layer.
The main local/dev validation paths already call that sync automatically:
./scripts/dev.sh --podman./scripts/dev.sh./scripts/test-worker.sh./scripts/test-checkout.shcd worker && npm run devcd worker && npm run deploynpm run test:premerge
What Still Requires Code
The platform now supports major customization without custom code, but not everything is intentionally configurable yet.
Still code-level today:
- adding new payment providers or checkout modes
- changing supported embed providers
- expanding CSP allowlists for arbitrary external hosts
- changing Stripe-owned field styling beyond the supported design-token bridge and Stripe’s appearance API
- introducing brand-new layout structures, page templates, or content block types
- changing font hosting/CSP behavior beyond the currently supported font stacks
Also note:
- not every Sass token is exposed on purpose
- not every Worker env var belongs in
_config.yml - the supported surface is curated to avoid security and maintenance regressions
- dashboard uploads commit files into the existing asset directories and update config/campaign fields; image/video uploads request the external media pipeline after commit, publish removes same-campaign dashboard-owned media that is no longer referenced, and adding a new upload category or storage backend is still code-level work
Safe Workflow For Forks
- Prefer the admin dashboard for supported settings/campaign/add-on edits. Use direct file edits when reviewing generated changes, changing unsupported fields, or working without the Worker.
- If editing files directly, update
_config.ymlor the relevant_campaigns/*.md. - Run
npm run sync:worker-configif you are editing config outside the normal entry points and want to refreshworker/wrangler.tomlimmediately. - Run:
npm run podman:doctor
./scripts/dev.sh --podman
- Verify:
- header/footer branding
- meta image / favicon
- campaign creator fallback
- CSP-sensitive pages still load without console CSP violations
- cart / checkout totals
- Stripe payment UI styling
- Manage Pledge
- supporter emails
- admin dashboard publish state, read-only secrets status, and role-scoped campaign visibility when dashboard fields changed
- Run the relevant checks:
npx vitest run tests/unit/config-boot.test.ts tests/unit/cart-provider.test.ts tests/unit/manage-page.test.ts tests/unit/worker-business-logic.test.ts
./scripts/podman-self-check.sh
Guidance For Future Additions
When adding new customization knobs, prefer this order:
- put the site-facing value in
_config.yml - mirror it to Worker env only if checkout, reports, or emails need it
- if it needs Worker env, update
scripts/sync-worker-config.rbinTOP_LEVEL_ORDER,DEV_ENV_ORDER, andbuild_mirror_values - document it here
- keep the supported surface curated instead of exposing every implementation detail
That keeps customization flexible without turning the platform into an unstable free-form theme engine.