Common Setup

Mail, storage, queue and cron, AI keys, payment gateways, push, social OAuth, headless Chrome and CDN.
Version: 1.2.0
Stack: Laravel 13 · Flutter 3.44 · MySQL

Item: MediaDesk — Multilingual AI-Powered Newsroom ERP

Where Settings Live

MediaDesk keeps third-party credentials in the admin panel, not in files: Settings → Integrations (/admin/settings/integrations). Values are stored encrypted in the settings table, override .env, and most groups have a Test connection button. .env keeps what is needed to boot: APP_KEY, database, cache/queue drivers. If you already filled .env, copy the values in once:

php artisan integrations:import-env

Third-party services and ongoing costs. MediaDesk works with external services that you sign up for and pay for yourself: hosting and domain, e-mail delivery (SMTP, Mailgun, Postmark, Amazon SES, Brevo), payment gateways (Stripe, PayPal, SSLCOMMERZ, bKash and the others; transaction and provider fees), Firebase Cloud Messaging for app push, AI providers (Anthropic, Google Gemini, OpenAI), object storage and CDN (Amazon S3, Cloudflare R2, Backblaze B2, Bunny), video hosting or embeds (YouTube and similar), and the Apple Developer Program and Google Play Console for the mobile app. You are responsible for your own accounts with these providers and for any current or future fees they charge; none of them is included in the item price. Every one of them is optional except hosting.

Who does what on this page: everything entered under Settings → Integrations (mail, AI, payments, push, storage, social) is an admin panel task: paste the value, press Save, then Test connection. Cron, queue worker, web server, Redis, headless Chrome and CDN rules are server administrator tasks. See Start Here: Who Does What.

Do not rotate APP_KEY after saving secrets: they are encrypted with it and would read as empty. If you use config:cache the Integrations overlay is still applied at runtime.

Settings → Integrations: groups on the left, fields and Test connection on the right
Settings → Integrations: groups on the left, fields and Test connection on the right

Mail

Used for newsletter confirmation, reader registration and password reset, contact form, newsletter digests, dunning and invoice mails. Open Integrations → Mail / SMTP: mailer (SMTP, Brevo API, Amazon SES, Mailgun, Postmark, sendmail, log), host, port, encryption, username, password, from address and name, and the contact-form recipient.

ProviderSettings
SMTP (any host)Mailer SMTP, host, port (587 with TLS / 465 with SSL), username, password
Brevo (API)Integrations → Brevo: API key; then choose mailer Brevo (API) in Mail / SMTP
Mailgun / Postmark / SESIntegrations → Mailgun / Postmark / SES, then choose the mailer
Local testingMAIL_MAILER=log (the default in .env.example): nothing is delivered, mail text is written to storage/logs/laravel.log
# .env equivalents
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"
[email protected]

Test: Tools → Diagnostics → Send test email (sends to your own address). For bulk sending set SENDER_MAIL_RATE (mails per second, default 10) below your provider’s limit and run a queue worker. Digest mails use the WebP image conversions, which Outlook desktop does not render.

Mail setting form
Mail setting form
Diagnostics: test e-mail, AI key, storage write, push
Diagnostics: test e-mail, AI key, storage write, push

Storage & Media

Uploads go through the media library on the disk named by MEDIA_DISK / Integrations → Media storage disk: public (default: storage/app/public served through the public/storage link), s3, r2 or b2.

Google Drive database backups (v1.2.0): open Settings → Integrations → Storage → Google Drive backups. Save enable/retention/minimum-copy settings, OAuth Client ID, Client Secret, Refresh Token and folder name/ID, then press Test connection. Client Secret and Refresh Token are encrypted in the database. These backup settings never use .env. Local SQL backups remain enabled even when Drive is off or unavailable; manage them under System → Operations.

  1. Local disk

    Run php artisan storage:link. Check readlink public/storage equals $(pwd)/storage/app/public (the link stores an absolute path; recreate it after moving the project). Back up storage/app/public.

  2. S3 / R2 / B2

    Integrations → Amazon S3 | Cloudflare R2 | Backblaze B2: key, secret, bucket, region/endpoint and the public/CDN base URL. Use Test connection (writes, reads and deletes a probe file). Then pick the disk in Media storage disk. The bucket (or the CDN origin) must be publicly readable. The S3 flysystem package is already in composer.json.

  3. Move existing files

    php artisan media:migrate-disk public s3 --dry-run, then without --dry-run; add --delete-source to remove local copies after a verified copy. Then set the new disk as default.

  4. Images and video

    Images use GD by default (IMAGE_DRIVER=gd, needs WebP support) or imagick. ffmpeg/ffprobe are optional (FFMPEG_PATH, FFPROBE_PATH) and only for video/audio tooling.

# .env equivalents
MEDIA_DISK=s3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=my-bucket
AWS_URL=https://cdn.example.com
AWS_ENDPOINT=                 # R2 / MinIO / Spaces
AWS_USE_PATH_STYLE_ENDPOINT=false

Raise upload_max_filesize / post_max_size in PHP and client_max_body_size in nginx (the sample uses 64m) if you upload video or PDFs.

Queue, Cron & Scheduler

Two processes keep MediaDesk alive. Without them the website still works, but campaigns, push, social posts, renewals, e-paper builds, counters and the ledger do nothing.

1. Scheduler (cron)
* * * * * cd /var/www/mediadesk && php artisan schedule:run >> /dev/null 2>&1

For development use php artisan schedule:work. Inspect with php artisan schedule:list. The registered entries are:

Demo safety: the demo switch is APP_DEMO (the only name; see the .env reference). Never set APP_DEMO=true on a production site: the nightly reset deletes content.

CommandSchedule
stats:flush, ads:flush, automation:tick, social:publish-due, marketing:campaigns-run, frontend:push-breaking, platform:heartbeat, platform:webhooks-retryevery minute
desk:fetch-sourcesevery 5 minutes
authors:notify-followersevery 10 minutes
membership:renew, health:check --recordhourly
finance:posthourly at :15
frontend:epaper-builddaily 05:30 (site time zone)
frontend:newsletter-send dailydaily 07:00 (site time zone)
ads:expiry-alertdaily 08:00 (site time zone)
frontend:newsletter-send weeklyFridays 08:00 (site time zone)
authors:close-month1st of the month, 02:00
demo:reset03:00, only when APP_DEMO=true and APP_ENV is not production
2. Queue worker

QUEUE_CONNECTION defaults to sync (jobs run inside the request). Production: redis (needs the phpredis extension; predis is not included) or database.

# database driver: the project has no jobs migration, create it first
php artisan queue:table
php artisan queue:failed-table
php artisan migrate

# run a worker (development)
php artisan queue:work --queue=default --sleep=3
; /etc/supervisor/conf.d/mediadesk-worker.conf
[program:mediadesk-worker]
command=php /var/www/mediadesk/artisan queue:work redis --sleep=3 --tries=1 --max-time=3600
user=www-data
numprocs=2
autostart=true
autorestart=true
stopasgroup=true
stopwaitsecs=3600
redirect_stderr=true
stdout_logfile=/var/log/mediadesk-worker.log

Campaign jobs are idempotent per recipient, so --tries=1 is intended. Optionally set SENDER_QUEUE=marketing and run queue:work --queue=marketing,default. After each deploy run php artisan queue:restart. Check System → System health (queue driver, failed jobs, scheduler heartbeat). php artisan mediadesk:doctor prints fix hints.

3. Confirm both are working (admin panel)
  1. Open System → System health

    Callout 1 in the screenshot.

  2. Queue row (2)

    Shows the driver in brackets. Queue (redis) or Queue (database) = Ok means a real queue is configured. Queue (sync) = Warn means jobs run inside the request (acceptable only for small sites).

  3. Failed jobs row (3)

    Should be 0. A growing number means jobs run but fail — see FAQ: queue worker.

  4. Writable rows (4)

    All Ok; otherwise see FAQ: permission errors.

  5. Scheduler row (5)

    Ok with a recent time means cron runs every minute. “No heartbeat yet” means cron is not running: see FAQ: cron.

System → System health with queue, failed jobs, writable folders and scheduler rows numbered
System → System health: (2) queue driver, (3) failed jobs, (4) folder permissions, (5) scheduler heartbeat. This capture is from a development machine, so Queue and Scheduler still show Warn.

Web Server, Cache & Redis

The document root is public/. The essentials of an nginx server block:

server {
    listen 443 ssl;
    server_name news.example.com;
    root /var/www/mediadesk/public;
    index index.php;
    client_max_body_size 64m;

    location /build/   { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; }
    location /storage/ { add_header Cache-Control "public, max-age=2592000"; try_files $uri =404; }
    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
    location ~ /\.(?!well-known) { deny all; }
}
  • Do not add your own Cache-Control for HTML: the app sends public, max-age=60, s-maxage=300, stale-while-revalidate=600 for stateless pages and no-store for forms. /sw.js must stay no-cache and be served from /.
  • Redis: CACHE_STORE=redis, SESSION_DRIVER=redis, QUEUE_CONNECTION=redis, plus REDIS_HOST, REDIS_PORT, REDIS_PASSWORD. Needs the phpredis extension.
  • Production caches: php artisan optimize (caches config, events, routes and views). Run php artisan optimize:clear before editing .env or code on the server, then optimize again.
  • OPcache: opcache.enable=1, memory_consumption=256, max_accelerated_files=20000; reload php-fpm on deploy if validate_timestamps=0.
  • Behind a proxy or CDN make sure TrustProxies is correct so https and client IPs are right (IP hashes drive throttling and spam controls).

AI Keys

Used by the AI copilot in the article editor and by AI intake. Open Integrations → AI default provider and choose one:

ProviderWhereDefault model
Anthropic (Claude)Integrations → Anthropic (AI): API key from console.anthropic.com. .env: ANTHROPIC_API_KEY, AI_MODELclaude-sonnet-5-5
Google GeminiIntegrations → Google Gemini (AI): API key, model, URLgemini-2.5-flash
OpenAI / ChatGPTIntegrations → OpenAI / ChatGPT (AI): API key, model, URLgpt-4o-mini
Step by step (admin panel)
  1. Get a key

    Create an API key in your provider’s console (console.anthropic.com, aistudio.google.com or platform.openai.com) and add billing there.

  2. Open Settings → Integrations → AI

    Callout 1, then the provider tab (2).

  3. Paste the key

    Into API key (3). Leave Model (4) and API URL on their defaults unless your provider tells you otherwise. Press Save (5).

  4. Test

    Test connection (6) sends one tiny request and shows OK or the provider’s error (wrong key, no credit, model not available).

  5. Choose the default provider

    Tab AI default provider: pick the provider whose key you added and save. The AI tools use this provider.

Settings → Integrations → AI → Anthropic with section, tab, API key, model, Save and Test connection numbered
AI keys: (1) AI section, (2) provider tab, (3) API key, (4) model, (5) Save, (6) Test connection. A saved key is never shown again; leave the box blank to keep it.
AI default provider tab with the provider select numbered
AI default provider: choose which provider the AI tools use.

Optional limits in .env: AI_PROVIDER (anthropic by default), AI_MAX_TOKENS (4000), AI_MONTHLY_TOKEN_LIMIT (newsroom soft budget, 0 = unlimited) and AI_USER_DAILY_CALLS (200). Click Test connection after saving. With no key the copilot says “not configured” and nothing is sent. You pay your provider directly; the AI paths were tested with faked HTTP.

Payment Gateways

Enter credentials in Integrations → Payments. A gateway is offered at checkout (memberships, ad invoices, donations, single articles) only when its keys exist. All gateways share one webhook route:

Third-party payment disclosure: Stripe, PayPal, SSLCOMMERZ and every other gateway listed below are external services. Buyers must open and maintain their own merchant accounts, supply their own credentials and webhooks, and pay all transaction, currency-conversion and provider fees.

POST https://YOUR-DOMAIN/membership/webhook/{gateway}
  stripe | paypal | sslcommerz | bkash | nagad | razorpay | mercadopago | iyzico | paytabs | paystack | nowpayments

It needs no CSRF token and no locale, is throttled to 120/minute, checks the provider signature (400 if invalid) and is idempotent. Browser returns land on /{locale}/membership/return/{invoice} (memberships) or /{locale}/payments/return/{number} (ad invoices, donations); the server re-confirms with the provider, the URL alone never marks anything paid. Every gateway has a sandbox toggle, on by default — test first.

GatewayCredentialsNotes
StripeSecret key (sk_test_…), webhook signing secret (whsec_…)Webhook events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired, payment_intent.succeeded, payment_intent.payment_failed, invoice.payment_succeeded, invoice.payment_failed. Only gateway with automatic off-session renewal. Local test: stripe listen --forward-to localhost:8000/membership/webhook/stripe. Test card 4242 4242 4242 4242.
PayPalClient id, secret, webhook idCreate a REST app (sandbox). Webhook events PAYMENT.CAPTURE.COMPLETED and PAYMENT.CAPTURE.DENIED. PayPal cannot settle BDT: price plans in USD or another PayPal currency.
SSLCOMMERZStore id, store password, sandboxSet the IPN URL to /membership/webhook/sslcommerz. Success only after verify_sign and the Validation API return VALID.
bKashApp key, app secret, username, password, sandboxTokenized checkout; no signed webhook, confirmed by the server-side execute call on return. BDT only.
NagadMerchant id, merchant private key, Nagad PG public key, sandboxBDT only. No public refund API: credit notes are marked manual.
RazorpayKey id, key secret, webhook secretWebhook event payment_link.paid.
Mercado PagoAccess token, webhook secretCheckout Pro; webhook topic Payments.
iyzicoAPI key, secret keyNo webhook: confirmed on browser return.
PayTabsProfile id, server key, regionWebhook with Signature header.
PaystackSecret keyWebhook event charge.success.
NOWPayments (crypto)API key, IPN secretIPN webhook; only status finished pays; refunds are manual.
Bank transfer (proof)Bank name, account name/number, SWIFT, instructionsReaders upload a receipt; staff approve in Membership → Bank transfers.
Manual / offline—Pending invoice, staff mark it paid. Instructions text via MEMBERSHIP_MANUAL_INSTRUCTIONS.
Step by step (admin panel + the gateway’s dashboard)
  1. Open Settings → Integrations → Payments

    Callout 1, then the gateway tab (2).

  2. Paste the API credentials

    Stripe: Secret key (3). Use sk_test_… first; switch to sk_live_… when you go live.

  3. Create the webhook in the gateway’s dashboard

    Use the URL for your gateway from the table below and select the events listed in the gateway table. The gateway then shows a signing secret (Stripe whsec_…, PayPal Webhook id, Razorpay/PayTabs/NOWPayments secret).

  4. Paste the webhook secret

    Into Webhook signing secret (4) and press Save (5).

  5. Test

    Test connection (6) checks the API key. Then buy a cheap plan with a test card and confirm in the gateway dashboard that the webhook delivery shows 200. A 400 means the secret does not match: see FAQ: webhook signature rejected.

Settings → Integrations → Payments → Stripe with secret key, webhook signing secret, Save and Test connection numbered
Stripe: (1) Payments section, (2) gateway tab, (3) secret key, (4) webhook signing secret, (5) Save, (6) Test connection
PayPal tab with client id, secret, webhook id and sandbox switch numbered
PayPal: (2) client id, (3) secret, (4) webhook id, (5) sandbox switch — on = test money; turn off for live, (6) Save
Webhook URL per gateway

Replace your-domain.com with your domain. Paste the URL exactly: https, no language prefix, no trailing slash.

GatewayWebhook / IPN URL to enter in the gateway dashboard
Stripehttps://your-domain.com/membership/webhook/stripe
PayPalhttps://your-domain.com/membership/webhook/paypal
SSLCOMMERZ (IPN)https://your-domain.com/membership/webhook/sslcommerz
Razorpayhttps://your-domain.com/membership/webhook/razorpay
Mercado Pagohttps://your-domain.com/membership/webhook/mercadopago
PayTabshttps://your-domain.com/membership/webhook/paytabs
Paystackhttps://your-domain.com/membership/webhook/paystack
NOWPayments (IPN)https://your-domain.com/membership/webhook/nowpayments
bKash, Nagad, iyzicoNo webhook to register: payment is confirmed by the server when the buyer returns
Bank transfer, ManualNo webhook: staff approve the payment

Currencies INR, BRL, TRY, NGN and AED are available for plans priced for the later gateways. Renewals and dunning: membership:renew hourly, reminders after 1, 3 and 5 days (MEMBERSHIP_DUNNING_DAYS), expiry after MEMBERSHIP_GRACE_DAYS (default 7). Reminders before the period ends: membership:remind hourly, 7, 3 and 1 days before by default (Settings → Public site → Membership, or MEMBERSHIP_REMINDER_DAYS). Only Stripe can charge a saved card automatically; every other gateway sends the reader a link to pay. Refunds create credit notes and a negative ledger entry.

Adapters were written from each provider’s public documentation and tested with faked HTTP only. Sandbox every gateway with your own merchant account before going live; the bank-transfer and manual options need no provider.

Push: VAPID & FCM

Web push (VAPID) — website and PWA
  1. Generate keys

    php artisan frontend:push-keys prints a fresh pair as .env lines and writes nothing. (Alternatively npx web-push generate-vapid-keys.)

  2. Save them

    Paste into Integrations → Web push (VAPID) (public key, private key, subject such as mailto:[email protected]) or into .env as PUSH_VAPID_PUBLIC_KEY, PUSH_VAPID_PRIVATE_KEY, PUSH_VAPID_SUBJECT, then php artisan config:clear.

  3. Check

    Without the public key the opt-in card never shows. The card appears after the 2nd article view or via the footer “Notifications” button. Web push needs HTTPS.

  4. Test

    Tools → Diagnostics shows key status and sends one test push; CLI: php artisan frontend:push-test --endpoint=<url>. 404/410 answers delete the subscription.

Settings → Integrations → Push & realtime → Web push (VAPID) with public key, private key, subject and Generate VAPID keys numbered
Web push: (1) Push & realtime section, (2) Web push tab, (3) public key, (4) private key, (5) subject, (6) Generate VAPID keys creates and saves a fresh pair for you

Your own keys, your own traffic. MediaDesk ships no VAPID key and no push endpoint of its own. The pair is generated on your server, the private key stays in your database, and each notification goes directly from your server to the endpoint the reader’s browser issued (Google, Mozilla or Apple push service). Nothing passes through the author’s servers. Generating a new pair makes existing subscribers subscribe again.

Firebase Cloud Messaging (FCM HTTP v1) — Android & iOS app

Third-party service disclosure: FCM is a Google Firebase service. You need your own Firebase project and account, and Google may charge for usage under its own pricing; these costs are not included in the item price.

  1. Create a Firebase project

    console.firebase.google.com; add your Android and iOS apps (see Mobile App → Firebase).

  2. Service account

    Project settings → Service accounts → Generate new private key (JSON).

  3. Save in MediaDesk

    Integrations → Firebase (FCM): project id and the full service-account JSON (or FCM_PROJECT_ID and FCM_SERVICE_ACCOUNT_JSON in .env). Optional: FCM_RETRIES (3), FCM_RETRY_MS (500), FCM_CHUNK (200 devices per job).

  4. Test one device

    php artisan fcm:test <device-token> --title="Hello" --body="Test" --url=/en/news/some-slug. When not configured it reports “not configured”.

Firebase (FCM) tab with project id, service account JSON, Save and Test connection numbered
Firebase (FCM): (1) Firebase tab, (2) project id from Firebase → Project settings, (3) the whole service-account JSON file content, (4) Save, (5) Test connection (fetches an access token from Google)

Push campaigns (Marketing → Push alerts) and breaking alerts send web push, then fan out FCM to registered devices that match language and topic (breaking, all, category). A campaign can send with FCM only. Article links are converted to mediadesk://article/{slug} for the app. A queue worker is required. The app registers its token through POST /api/v1/devices.

Push was verified against faked HTTP only. Send a real test to a real device from your own Firebase project before announcing the feature.

Social OAuth & Telegram

For Automation → Social accounts & UTM. Create a developer app at each network, then put the client id/secret in Integrations → Social. Register this redirect URI exactly (replace the domain and provider):

https://YOUR-DOMAIN/admin/social/oauth/facebook/callback
https://YOUR-DOMAIN/admin/social/oauth/linkedin/callback
https://YOUR-DOMAIN/admin/social/oauth/x/callback
NetworkCredentialsScopes requested
Facebook (pages)OAuth client id and secret (SOCIAL_FACEBOOK_CLIENT_ID/_SECRET), or a Page access tokenpages_show_list, pages_manage_posts, pages_read_engagement
LinkedInClient id and secret (SOCIAL_LINKEDIN_*), or an access tokenw_member_social, w_organization_social
XClient id and secret (SOCIAL_X_*), OAuth 2.0 with PKCE, or a user bearer tokentweet.read, tweet.write, users.read, media.write, offline.access
TelegramBot token from @BotFather (SOCIAL_TELEGRAM_BOT_TOKEN); add the bot to your channel, use the channel handle in the account—

Then open Automation → Connect accounts (/admin/social/oauth) and press Connect. Keep an account on the log driver (dry run) until you are happy with the output. Reader sign-in with Google: create an OAuth client and register https://YOUR-DOMAIN/account/google/callback, then enter id and secret in Integrations → Google sign-in.

Connect accounts with OAuth
Connect accounts with OAuth
Social accounts (demo accounts use the dry-run driver)
Social accounts (demo accounts use the dry-run driver)

Social publishing was tested with faked HTTP only. Each network reviews apps and may restrict posting permissions; follow their app-review process.

Headless Chrome

Two features use a headless Chrome/Chromium binary on the server:

  • E-paper PDFs (php artisan frontend:epaper-build, scheduled daily 05:30, site time zone). Chrome opens /{locale}/e-paper/{date}/print and prints to PDF. Auto-detected binaries: macOS Chrome/Chromium, google-chrome, chromium. Override with EPAPER_CHROME=/usr/bin/google-chrome. EPAPER_BASE_URL (default APP_URL) must be reachable from the server itself. Languages need at least 30 published translations (EPAPER_MIN_STORIES). Downloads: EPAPER_DOWNLOAD=free|premium.
  • Social cards — text is rendered by Chrome for correct Bangla conjuncts and Arabic joining; Integrations → Social cards auto uses Chrome when present, else GD.
# Debian / Ubuntu example
sudo apt install -y chromium fonts-noto fonts-noto-core
# or Google Chrome from Google's apt repo, then:
php artisan frontend:epaper-build --locale=en --force
# one date / one language
php artisan frontend:epaper-build YYYY-MM-DD --locale=bn --edition=dhaka

Install Bangla and Arabic capable fonts (for example Noto) on the server or the PDF will show blanks. If the build fails the edition row shows status failed; the command prints a clear error when no Chrome binary is found.

CDN

Put Cloudflare, Bunny or CloudFront in front of the origin. HTML is cacheable (s-maxage=300, no cookies on stateless pages).

  • Cache by full URL; do not strip ETag; vary only on Accept-Encoding.
  • Bypass cache for the paths below (details after this list). Requests other than GET/HEAD are never cached by CDNs by default; leave it that way.
  • Purge: Integrations → CDN purge — driver log (default, only logs), cloudflare (zone id + API token), bunny (pull zone id + API key) or generic url (endpoint + secret). System → Maintenance / CDN has a purge button and shows the driver status.
  • Media on S3/R2/B2 can use the CDN base URL as AWS_URL.
Paths to bypass (copy into your CDN dashboard)

Create one rule “URI path starts with … → Bypass cache” (Cloudflare: Caching → Cache Rules; Bunny: Edge Rules → Override cache time 0; CloudFront: a behaviour with the CachingDisabled policy) and add each line below as its own value. Every line is a plain path: no wildcards, no regular expressions.

/admin
/dashboard
/login
/logout
/forgot-password
/reset-password
/password
/install
/demo-login
/account/google
/membership/webhook
/broadcasting
/sanctum
/api/v1/auth
/api/v1/me
/api/v1/devices
/sw.js

Then the same eight paths for each language your site uses. The four built-in languages are shown; add eight more lines for every language you add (with its code, for example /de/).

/en/account
/en/advertiser
/en/membership
/en/payments
/en/newsletter
/en/csrf
/en/preview
/en/search/suggest

/bn/account
/bn/advertiser
/bn/membership
/bn/payments
/bn/newsletter
/bn/csrf
/bn/preview
/bn/search/suggest

/ar/account
/ar/advertiser
/ar/membership
/ar/payments
/ar/newsletter
/ar/csrf
/ar/preview
/ar/search/suggest

/fr/account
/fr/advertiser
/fr/membership
/fr/payments
/fr/newsletter
/fr/csrf
/fr/preview
/fr/search/suggest

“Starts with” matters: /en/newsletter also covers the signed confirmation, unsubscribe and preferences links under it, and /admin covers every admin screen. If your CDN only offers “equals”, use its “starts with” or “wildcard” option instead; an exact-match rule would miss the pages below each path. On Cloudflare also add a WAF skip rule for /membership/webhook so payment webhooks are never challenged.

System → Maintenance / CDN: maintenance, media storage and CDN purge
System → Maintenance / CDN: maintenance, media storage and CDN purge

Captcha, Google, Pusher

  • Captcha — Integrations → Captcha: none, reCAPTCHA v2/v3 or Cloudflare Turnstile with site key and secret (and minimum score for v3). Applies to register, login, contact, comments and newsletter forms.
  • Google Indexing API — paste a service-account JSON (owner in Search Console), then enable it in SEO.
  • Brevo, Mailgun, Postmark, SES — see Mail.
  • Pusher / Ably — optional realtime broadcasting credentials (PUSHER_APP_ID, PUSHER_APP_KEY, PUSHER_APP_SECRET, PUSHER_APP_CLUSTER).
  • GA4 / Meta Pixel / AdSense — ids in Settings → Public site; loaded only after cookie consent.

.env Reference

Keys that matter in a typical install (see .env.example). Most credentials can instead be entered in Integrations. The demo switch is APP_DEMO everywhere in the code, .env.example and this documentation.

KeyDefault / examplePurpose
APP_ENV, APP_DEBUG, APP_URLproduction, false, https originAPP_URL must match the real scheme+host: used in links, signed URLs, sitemaps
SITE_TIMEZONE, APP_CURRENCYUTC, USDDefault public time zone and base currency (both can be changed later in the admin). The database always stores UTC
DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORDmysql, …Written by the installer
CACHE_STORE, SESSION_DRIVER, QUEUE_CONNECTIONfile, file, syncUse redis in production
SESSION_SECURE_COOKIEtrue behind HTTPSSecure cookies
MEDIA_DISK, AWS_*publicMedia storage
MAIL_*, FRONTEND_CONTACT_TOlogMail (or Integrations)
SENDER_MAIL_RATE, SENDER_QUEUE10, unsetNewsletter throttle and queue
PUSH_VAPID_PUBLIC_KEY, PUSH_VAPID_PRIVATE_KEY, PUSH_VAPID_SUBJECTunsetWeb push
FCM_PROJECT_ID, FCM_SERVICE_ACCOUNT_JSONunsetApp push
ANTHROPIC_API_KEY, AI_PROVIDER, AI_MODEL, AI_MONTHLY_TOKEN_LIMIT, AI_USER_DAILY_CALLSanthropic, claude-sonnet-5-5, 0, 200AI copilot
STRIPE_SECRET, STRIPE_WEBHOOK_SECRET, SSLCOMMERZ_*, BKASH_*, NAGAD_*, PAYPAL_*unset, sandbox trueOriginal five gateways (the rest are Integrations-only)
MEMBERSHIP_GRACE_DAYS, MEMBERSHIP_DUNNING_DAYS, MEMBERSHIP_MANUAL_INSTRUCTIONS7, 1,3,5Renewals
MEMBERSHIP_REMINDER_DAYS, MEMBERSHIP_REMINDER_ENDING_DAYS7,3,1 and 3Reminders before renewal, and the single notice for a membership that will not renew. The Admin setting overrides the first one.
EPAPER_CHROME, EPAPER_BASE_URL, EPAPER_DOWNLOAD, EPAPER_MIN_STORIESauto, APP_URL, free, 30E-paper
SEO_INDEXABLE, SEO_AI_CRAWLERS, SEO_SITEMAP_MODEfalse, allow, flatIndexing
FRONTEND_COMMENTS_AUTO_APPROVEfalseHold comments for moderation
CONTENT_FETCH_ENABLEDfalseLive Bangladesh weather/rates/DSE fetch
APP_DEMOfalsePublic demo switch: demo login buttons, read-only demo role and demo logins, nightly demo:reset (non-production only). Never true on a real site
DEMO_READONLY_ACCOUNTStrueOnly with APP_DEMO=true: makes the published demo logins read-only
PUSHER_*, CDN_PURGE_DRIVER, CLOUDFLARE_*, BUNNY_*unsetOptional

Check It Works

php artisan mediadesk:doctor          # PHP, folders, DB, cache/queue/mail, optional tools, with fix hints
php artisan health:check              # system health checks (exit code 1 on failure)
php artisan schedule:list             # confirm scheduled commands
php artisan queue:failed              # failed jobs
php artisan frontend:marketing-stats  # subscribers per list / push subscriptions
tail -n 50 storage/logs/laravel.log

Symptom-by-symptom help: Troubleshooting FAQ. In the browser: System → System health and System → Diagnostics (test e-mail, AI key, storage write, push key status and a test push).

System health
System health