| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/client-portal-laravel/releases/20260626120707/config/ |
Upload File : |
<?php
declare(strict_types=1);
/**
* ScopeForged Websites Product Configuration
*
* Configuration for websites.scopeforged.com. Phase 0 ships a single-page
* landing with a Calendly inline embed and lead form. Later phases extend
* this config (pricing engine, builder, provisioning).
*
* @see docs/plans/website-hosting/270-scopeforged-websites-engineering-plan.md
*/
return [
/*
|--------------------------------------------------------------------------
| Calendly Inline Embed URL
|--------------------------------------------------------------------------
|
| Public Calendly event URL embedded on the landing. Leave empty to hide
| the embed entirely (the "book a call" CTA falls back to a plain mailto
| link). Setting WEBSITES_CALENDLY_URL=https://calendly.com/... enables it.
|
*/
'calendly_url' => env('WEBSITES_CALENDLY_URL', ''),
/*
|--------------------------------------------------------------------------
| Contact Email
|--------------------------------------------------------------------------
|
| Fallback contact address shown on the landing when Calendly is disabled
| and as a "prefer email" option alongside the form.
|
*/
'contact_email' => env('WEBSITES_CONTACT_EMAIL', env('MARKETING_CONTACT_EMAIL', 'hello@scopeforged.com')),
/*
|--------------------------------------------------------------------------
| Marketing Copy
|--------------------------------------------------------------------------
|
| Strings rendered on the landing. Centralized so non-engineering edits
| live in one place and don't require touching the Blade view.
|
*/
/*
|--------------------------------------------------------------------------
| Builder assets
|--------------------------------------------------------------------------
|
| Storage disk for builder-session asset uploads (logos, etc.). Plan 270
| §Phase 2 stores binaries under tenants/anon/{session}/... or
| tenants/{client_id}/builder/{session}/... on S3 in production. The
| `scan_inline_for_dev` flag controls whether the asset row is marked
| clean at upload time (dev/scaffolding) or stays pending until a
| future virus-scan job flips it (production).
|
*/
'assets' => [
'disk' => env('WEBSITES_ASSETS_DISK', env('FILESYSTEM_DISK', 'local')),
'scan_inline_for_dev' => env('WEBSITES_ASSETS_SCAN_INLINE_FOR_DEV', true),
],
/*
|--------------------------------------------------------------------------
| Proposals
|--------------------------------------------------------------------------
|
| Storage disk for rendered proposal PDFs. The PDF is persisted so the
| customer-facing page can stream it without re-rendering on every hit.
| Default validity window applies when admin doesn't specify one.
|
*/
'proposals' => [
'disk' => env('WEBSITES_PROPOSALS_DISK', env('FILESYSTEM_DISK', 'local')),
'default_validity_days' => env('WEBSITES_PROPOSAL_VALIDITY_DAYS', 14),
],
/*
|--------------------------------------------------------------------------
| Data exports (plan 270 §Phase 4)
|--------------------------------------------------------------------------
|
| Per GDPR Article 20 every paying customer can download all data we
| hold about them. The job lands in the `client-portal-default` queue
| per the plan; storage disk defaults to the global filesystem disk so
| S3 in prod / local in dev "just works" without per-feature env vars.
|
*/
'exports' => [
'disk' => env('WEBSITES_EXPORTS_DISK', env('FILESYSTEM_DISK', 'local')),
'queue' => env('WEBSITES_EXPORTS_QUEUE', 'client-portal-default'),
],
/*
|--------------------------------------------------------------------------
| AI content generation (plan 270 §Phase 6)
|--------------------------------------------------------------------------
|
| `provider` chooses the bound ContentGenerator implementation:
| - "fake" (default for dev + CI)
| - "anthropic" primary in production
| - "openai" documented fallback
|
| Setting `WEBSITES_AI_PROVIDER=anthropic` in env switches production.
| `default_monthly_token_budget` is the per-tenant hard cap unless an
| ai_usage_quotas row overrides it for the (client_id, year_month).
|
*/
'ai' => [
'provider' => env('WEBSITES_AI_PROVIDER', 'fake'),
'fallback_provider' => env('WEBSITES_AI_FALLBACK_PROVIDER', 'openai'),
'default_monthly_token_budget' => env('WEBSITES_AI_DEFAULT_MONTHLY_BUDGET', 200_000),
],
/*
|--------------------------------------------------------------------------
| Cost monitoring (plan 272 §A.2)
|--------------------------------------------------------------------------
|
| `ops_alert_email` is the address that receives the 50% rung
| TenantCostGuard alert. Empty disables the ops rung (the alert
| still records in tenant_cost_alerts_sent so duplicate hourly
| runs don't refire).
|
*/
'costs' => [
'ops_alert_email' => env('WEBSITES_COSTS_OPS_ALERT_EMAIL', ''),
],
/*
|--------------------------------------------------------------------------
| Security (plan 272 §B.3, §B.4)
|--------------------------------------------------------------------------
|
| `require_mfa_for_new_customers` (default true) makes
| WebsiteOnboardingCompletionService stamp a `requires_mfa_setup_by`
| deadline on the User row; the `RequireMfaSetup` middleware
| redirects past the deadline.
|
| Session idle-timeout minutes are role-derived; the
| `EnforceSessionIdleTimeout` middleware logs the session out
| after the threshold.
|
*/
'security' => [
'require_mfa_for_new_customers' => env('WEBSITES_REQUIRE_MFA_FOR_NEW_CUSTOMERS', true),
'mfa_setup_grace_days' => env('WEBSITES_MFA_SETUP_GRACE_DAYS', 7),
'session_idle_timeout_customer_minutes' => env('WEBSITES_SESSION_IDLE_CUSTOMER_MIN', 720),
'session_idle_timeout_admin_minutes' => env('WEBSITES_SESSION_IDLE_ADMIN_MIN', 240),
],
/*
|--------------------------------------------------------------------------
| Mail compliance (plan 270 §Phase 7)
|--------------------------------------------------------------------------
|
| Physical address rendered in every customer-facing email's CAN-SPAM
| footer. CanSpamFooterEnforcer asserts the address is present in
| every rendered body.
|
*/
'mail' => [
'physical_address' => env('WEBSITES_MAIL_PHYSICAL_ADDRESS', 'ScopeForged Inc., PO Box change-me, City, ST'),
],
/*
|--------------------------------------------------------------------------
| CSP (plan 270 §Phase 7)
|--------------------------------------------------------------------------
|
| Platform-level baseline merged with per-template extensions by
| CspHeaderComposer. Source allowlists prevent templates from
| silently introducing new third-party dependencies.
|
*/
'csp' => [
'baseline' => [
'default-src' => ["'self'"],
'script-src' => ["'self'"],
'frame-src' => ["'none'"],
'img-src' => ["'self'", 'data:', 'https://cdn.scopeforged.com'],
'style-src' => ["'self'", "'unsafe-inline'"],
'connect-src' => ["'self'"],
'font-src' => ["'self'", 'data:'],
'object-src' => ["'none'"],
],
'report_uri' => env('WEBSITES_CSP_REPORT_URI', '/csp-report'),
'report_only_by_default' => env('WEBSITES_CSP_REPORT_ONLY', true),
],
/*
|--------------------------------------------------------------------------
| Analytics
|--------------------------------------------------------------------------
|
| Server-side event capture per plan 270 §Phase 1. The salt is used to
| hash IP and User-Agent so the analytics table never stores raw PII;
| rotating the salt invalidates the ability to link future events to
| past hashed identifiers (an intentional privacy property).
|
*/
'analytics' => [
'enabled' => env('WEBSITES_ANALYTICS_ENABLED', true),
'hash_salt' => env('WEBSITES_ANALYTICS_HASH_SALT', env('APP_KEY', 'change-me-in-env')),
'excluded_paths' => [
'sitemap.xml',
'robots.txt',
'leads',
],
],
'copy' => [
'hero_eyebrow' => 'ScopeForged Websites',
'hero_headline' => 'Done-for-you websites for service businesses.',
'hero_subhead' => 'Conversion-focused sites, transparent pricing, no plugin rabbit holes. We design it, build it, and keep it running so you can stay focused on the work.',
'primary_cta' => 'Book a 20-minute call',
'secondary_cta' => 'Send us a brief',
'response_promise' => 'We reply within one business day.',
],
/*
|--------------------------------------------------------------------------
| DNS + Cache driver wiring (Plan 273 §A)
|--------------------------------------------------------------------------
|
| Production binds the real Cloudflare classes; tests + dev fall back to
| the in-memory fakes. Switching is a one-line env change (`fake` vs
| `cloudflare`) — no code edits.
|
*/
'dns' => [
'driver' => env('WEBSITES_DNS_DRIVER', 'fake'),
],
/*
|--------------------------------------------------------------------------
| Custom-domain self-service (Plan 279)
|--------------------------------------------------------------------------
|
| `self_serve_enabled` kill-switch — flip to false to block new attaches
| without breaking existing Domain rows. Per-Client granularity is a
| follow-up. `public_ipv4`/`public_ipv6` are surfaced to customers in
| the wizard's "Required DNS records" panel. `default_grace_days` is
| the pre-selected option in the remove modal. The reverify ladder
| (30d / 60d / 90d) is enforced by ReverifyDomainOwnershipJob.
|
*/
'custom_domain' => [
'self_serve_enabled' => env('WEBSITES_CUSTOM_DOMAIN_SELF_SERVE', true),
'public_ipv4' => array_values(array_filter(array_map('trim', explode(',', (string) env('WEBSITES_PUBLIC_IPV4', ''))))),
'public_ipv6' => array_values(array_filter(array_map('trim', explode(',', (string) env('WEBSITES_PUBLIC_IPV6', ''))))),
'default_grace_days' => (int) env('WEBSITES_CUSTOM_DOMAIN_DEFAULT_GRACE_DAYS', 30),
'reverify_interval_days' => (int) env('WEBSITES_REVERIFY_INTERVAL_DAYS', 30),
'reverify_failure_threshold_days' => (int) env('WEBSITES_REVERIFY_FAILURE_THRESHOLD_DAYS', 60),
'reverify_stale_threshold_days' => (int) env('WEBSITES_REVERIFY_STALE_THRESHOLD_DAYS', 90),
'restart_cap_per_24h' => (int) env('WEBSITES_DOMAIN_RESTART_CAP', 10),
],
'cache_purge' => [
'driver' => env('WEBSITES_CACHE_PURGE_DRIVER', 'fake'),
'zone_id' => env('WEBSITES_CLOUDFLARE_ZONE_ID', ''),
],
/*
|--------------------------------------------------------------------------
| Apache SNI snippet regeneration (Plan 273 §D.1)
|--------------------------------------------------------------------------
|
| Path the catch-all vhost includes for per-host cert directives. Dry-run
| is on by default so a misconfigured stub can't take Apache down on the
| first deploy; flip via env once the rendered output looks right.
|
*/
'apache' => [
'ssl_domains_conf_path' => env('WEBSITES_APACHE_SSL_DOMAINS_CONF', '/etc/apache2/ssl-domains.conf'),
'reload_command' => env('WEBSITES_APACHE_RELOAD_COMMAND', '/usr/sbin/apachectl graceful'),
'reload_dry_run' => env('WEBSITES_APACHE_RELOAD_DRY_RUN', true),
'queue' => env('WEBSITES_APACHE_QUEUE', 'client-portal-default'),
],
/*
|--------------------------------------------------------------------------
| SMS gateway routing (Plan 273 §B.3)
|--------------------------------------------------------------------------
*/
'sms' => [
'per_tenant_rate_per_minute' => env('WEBSITES_SMS_PER_TENANT_RPM', 60),
],
/*
|--------------------------------------------------------------------------
| Backup integrity (Plan 273 §F.1)
|--------------------------------------------------------------------------
*/
'backup' => [
'inventory_disk' => env('WEBSITES_BACKUP_INVENTORY_DISK', 'backups'),
'asset_disk' => env('WEBSITES_BACKUP_ASSET_DISK', 'assets'),
'queue' => env('WEBSITES_BACKUP_QUEUE', 'client-portal-default'),
],
/*
|--------------------------------------------------------------------------
| Per-tenant content search (Plan 273 §G.2)
|--------------------------------------------------------------------------
|
| MysqlSearchBackend is the primary; Meilisearch is the mirror during
| the FULLTEXT -> Meilisearch transition. Once SearchParityCheckJob
| (§G.3) confirms >= 99% top-K parity for 7 days per tenant, the
| enabled_clients list flips reads to Meilisearch for that client_id.
|
*/
'search' => [
'meilisearch' => [
'url' => env('WEBSITES_MEILISEARCH_URL', 'http://127.0.0.1:7700'),
'master_key' => env('WEBSITES_MEILISEARCH_MASTER_KEY', ''),
'mirror_writes' => (bool) env('WEBSITES_MEILISEARCH_MIRROR_WRITES', false),
'enabled_clients' => array_filter(array_map('intval', explode(',', (string) env('WEBSITES_MEILISEARCH_CLIENTS', '')))),
],
],
];