Skip to content

0franco/symfony-stripe-starter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

73 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Build a Stripe-Powered Shop in Minutes πŸš€

Banner

A modern, production-ready Symfony boilerplate for building payment-enabled storefronts with Stripe.

This project provides a robust foundation for integrating Stripe Checkout, Customer Portals, and Webhooks while leveraging the latest Symfony 8 features and FrankenPHP.

✨ Key Features

  • Stripe Integration:
    • πŸ›’ Dynamic Product Catalog: Fetches products and prices directly from Stripe with local caching for optimal performance.
    • πŸ” Advanced Search & Filtering: Fast, metadata-aware search that scans product names, descriptions, and custom Stripe metadata.
    • 🏷️ Price Type Toggle: Modern UI to quickly switch between one-time purchases and recurring subscriptions.
    • πŸ’³ Stripe Checkout: Seamlessly handles one-time payments and recurring subscriptions with pre-built Stripe pages.
    • πŸ”„ Customer Portal: Pre-configured access for customers to manage their billing, view invoices, and update subscriptions.
    • βš“ Webhook Handling: Robust webhook controller for processing payment success, subscription updates, and cancellations in real-time.
    • πŸ“¦ Order History & Entitlements: Automatically tracks purchases and subscriptions locally as Order entities for easy access and entitlement checks.
    • β™₯️ Wishlist: Users can save products for later, it set a cookie that persists across sessions for 1 year.
    • 🎧 Single product mode: Hide product catalog and redirect to a single product page, useful for single-product stores or demos.
  • Authentication & Security:
    • πŸ”‘ Passwordless Login: Modern magic link authentication systemβ€”no passwords to remember, just simple email-based sign-ins.
    • πŸ‘€ Automatic User Provisioning: New users are automatically created upon successful Stripe checkout, ensuring a seamless onboarding flow.
    • πŸ›‘οΈ CSRF Protection: Secure form handling across the application.
  • Modern Frontend: Built with Symfony UX, Tailwind CSS v4, Stimulus, and Turbo.
    • ✨ Polished UI: Sleek, responsive design using a refined Slate color palette, sophisticated typography, and smooth transitions.
    • πŸŒ“ Collapsible Search: Minimalist interface that stays collapsed by default to focus on products, expanding automatically when filters are active.
    • πŸ“± Mobile Optimized: Fully responsive layout with touch-friendly elements and adaptive spacing for all screen sizes.
    • ⚑ No-Build Workflow: Powered by AssetMapperβ€”modern JavaScript and CSS management without the overhead of Node.js build steps.
  • Developer Experience:
    • ⚑ FrankenPHP: High-performance PHP application server with built-in Caddy and automatic HTTPS.
    • 🐳 Dockerized: Fully containerized environment for consistent development and easy production deployment.
    • 🌐 Multilingual: Internationalized (i18n) using Symfony Translation. Supports English, Spanish, and Italian with a session-based language switcher.
    • πŸ—ƒοΈ SQLite by Default: Zero-config database setup for lightning-fast prototyping and local development, with optional Litestream replication for production persistence.

πŸ› οΈ Tech Stack

  • PHP 8.4+
  • Symfony 8.0
  • Stripe PHP SDK
  • FrankenPHP (with Caddy)
  • AssetMapper (Modern JS/CSS management)
  • Tailwind CSS
  • Doctrine ORM (SQLite)

🌐 Live Demo

Click to see live demo


πŸš€ Getting Started

1. Prerequisites

  • Docker and Docker Compose installed.
  • A Stripe Account (for API keys).

2. Installation

Clone the repository:

git clone https://github.com/0franco/stripe-php-frontend
cd stripe-php-frontend

3. Configure Environment

Copy the example environment file:

cp .env .env.local

Open .env.local and set the following variables:

# Required: Set these to your Stripe keys
STRIPE_SECRET_KEY=sk_test_...

# Required: Set to allow user provisioning and order linking
STRIPE_WEBHOOK_SECRET=whsec_...

# Required: To use email notifications, user needs it to login
MAILER_DSN=resend+api://your-api-key@default

# Optional: Set this to a comma-separated list of product IDs to show only those products, requires SHOW_ALL_PRODUCTS=false
ALLOWED_PRODUCT_IDS=prod_1,prod_2,..

# Optional: Set these to true to enable single-product mode, which hides the product catalog and redirects to a single product page, requires DEFAULT_PRODUCT_ID set
SINGLE_PRODUCT_MODE=true

# Optional: Remove any languages you don't need, defaults to es,en,it
AVAILABLE_LANGS=es,en,it

# Optional: Recommeded to be enabled for filtering login bots
CLOUDFLARE_TURNSTILE_SITE_KEY=
CLOUDFLARE_TURNSTILE_SECRET_KEY=

4. Start the Application

Build and start the containers:

docker compose build --pull
docker compose up -d

The app will be available at https://localhost.

5. Initialize Database

Run migrations inside the PHP container:

docker compose exec php bin/console doctrine:migrations:migrate --no-interaction

πŸ—„οΈ Database Persistence

This project uses SQLite by default. Locally, the database lives at db/data.db.

In production (e.g. Render, Fly.io), containers are ephemeral β€” the database is wiped on every deploy unless you configure persistence.

Option A β€” Litestream + Cloudflare R2 (Recommended)

Litestream continuously replicates your SQLite WAL to object storage. On container start, it restores the latest snapshot before the app boots. This gives you near-zero data loss with no infrastructure changes.

Why Cloudflare R2? Zero egress fees, S3-compatible API, and generous free tier.

1. Create a Cloudflare R2 Bucket

  1. Go to Cloudflare Dashboard β†’ Storage & Databases β†’ R2 Object Storage
  2. Click Create Bucket, name it (e.g. myapp-db)
  3. On the R2 Overview page, note your Account ID (top right)

2. Create R2 API Tokens

  1. On the R2 Overview page, click Manage API Tokens
  2. Click Create API Token
  3. Set Permissions to Object Read & Write, scoped to your bucket
  4. Save the Access Key ID and Secret Access Key β€” the secret is shown only once

3. Configure Environment Variables

Add to your .env.local (local) or your PaaS environment panel (production):

LITESTREAM_REPLICA_URL="s3://your-bucket-name/data.db"
CLOUDFLARE_ACCOUNT_ID=your-account-id
LITESTREAM_ACCESS_KEY_ID=your-access-key-id
LITESTREAM_SECRET_ACCESS_KEY=your-secret-access-key

The LITESTREAM_REPLICA_URL uses the S3-compatible format β€” the R2 endpoint is configured automatically via CLOUDFLARE_ACCOUNT_ID.

4. How It Works

When LITESTREAM_REPLICA_URL is set, the container entrypoint will:

  1. Restore the latest database snapshot from R2 (if no local DB exists)
  2. Run pending migrations
  3. Start Litestream replication in the background
  4. Boot the app normally

When the env var is not set (local dev), the app falls back to the local db/data.db file and requires manual initialization.

⚠️ Single-instance only. SQLite with Litestream does not support horizontal scaling. Keep your service at a single instance.


Option B β€” Litestream + AWS S3

Same setup as above, replacing the R2-specific config:

LITESTREAM_REPLICA_URL="s3://your-s3-bucket-name/data.db"
LITESTREAM_ACCESS_KEY_ID=your-aws-access-key
LITESTREAM_SECRET_ACCESS_KEY=your-aws-secret-key
# Leave CLOUDFLARE_ACCOUNT_ID unset

Update frankenphp/litestream.yml to remove the endpoint and force-path-style fields β€” standard S3 doesn't need them:

dbs:
  - path: /app/db/data.db
    replicas:
      - name: s3
        url: ${LITESTREAM_REPLICA_URL}
        access-key-id: ${LITESTREAM_ACCESS_KEY_ID}
        secret-access-key: ${LITESTREAM_SECRET_ACCESS_KEY}
        region: us-east-1  # your bucket region

πŸ“§ Email (Transactional)

This app uses magic link authentication, so transactional email is required for login to work.

Local Development

The included Mailpit service catches all outgoing emails. No configuration needed β€” visit http://localhost:8025 to view sent emails.

Production

⚠️ PaaS platforms like Render block outbound SMTP ports (25, 465, 587) on free and starter plans. Standard SMTP will time out. You must use an HTTP API-based email provider instead.

The recommended provider is Resend β€” generous free tier, simple API, and first-class Symfony Mailer support.

MAILER_DSN=resend+api://your-api-key@default

See the Resend + Symfony docs for domain verification setup, which is required to send from your own email address.


Single Product Mode

Want to showcase just one product and skip the full catalog? Enable Single Product Mode:

  1. Set these environment variables:

    SINGLE_PRODUCT_MODE=true
    DEFAULT_PRODUCT_ID=prod_1234567890

    Replace prod_1234567890 with your actual product ID.

  2. What happens?
    All users will be automatically redirected to the page for the specified product, bypassing the catalog view.


Show only certain products

If you want to show only a subset of products, set on your environment as below:

SHOW_ALL_PRODUCTS=false
ALLOWED_PRODUCT_IDS=prod_1234567890,prod_9876543210 #Comma separated list of product ids

🌐 Internationalization (i18n)

This project uses the Symfony Translation component for multi-language support.

Supported Languages

  • English (en) - Default
  • Spanish (es)
  • Italian (it)

How it works

  • Translation Files: Located in the translations/ directory (e.g., messages.en.yaml).
  • Language Switcher: A compact dropdown menu in the header allows users to switch between supported languages.
  • Locale Persistence: The selected locale is stored in the user's session and applied to subsequent requests via a custom LocaleListener.
  • Templates: Uses the |trans filter in Twig templates. The lang attribute in base.html.twig is automatically set based on the current locale.
  • Controllers: The TranslatorInterface is used to translate flash messages and other dynamic content.

Adding a new language

  1. Create a new YAML file in translations/ using the appropriate locale code: messages.[locale].yaml.
  2. Copy the keys from messages.en.yaml and provide the translations.
  3. Clear the cache to register the new translation file:
    docker compose exec php bin/console cache:clear

Related Products

You can link products together by adding a related_ids metadata key on any Stripe product.

Setup

  1. Go to your Stripe Dashboard
  2. Open a product and scroll to Metadata
  3. Add a new key/value pair:
Key Value
related_ids prod_xxx,prod_yyy,prod_zzz

The value is a comma-separated list of Stripe product IDs.

Example

related_ids = prod_ABC123,prod_DEF456

These products will appear in a Related Products carousel at the bottom of the product detail page.

Notes

  • Order is preserved (left to right in carousel)
  • Inactive or deleted product IDs are silently skipped
  • There is no hard limit, but 4–6 items is recommended for UX

Footer links

Modify footer links by changing FOOTER_LINKS env.

Simple: label,url pairs separated by |

FOOTER_LINKS="Home,/|Terms of Service,/terms, External link,https://example.com/terms"

Google Analytics

Integrated with Google Analytics, to track add the code on GA4_MEASUREMENT_ID env var.

GA4_MEASUREMENT_ID=G-XXXXXXXXXXX

πŸ’‘ Development Workflows

Stripe Webhooks

To test webhooks locally, use the Stripe CLI:

stripe listen --forward-to https://localhost/webhooks/stripe --skip-verify

Update your STRIPE_WEBHOOK_SECRET in .env.local with the signing secret provided by the CLI.

Clearing Stripe Cache

The product catalog is cached for performance. To refresh it:

docker compose exec php bin/console app:clear-stripe-cache

Running Tests

docker compose exec php bin/console phpunit

Product Metadata and SEO

templates/product/details.html.twig reads Stripe product metadata and applies specific keys to SEO:

  • meta_title: Used as the page <title> (fallback: Stripe product name).
  • meta_description: Used as <meta name="description"> (fallback: Stripe product description).

These SEO keys are intentionally excluded from the visible "Product details" metadata table.

The template defines a local blacklist constant:

{% set METADATA_BLACKLIST = ['meta_title', 'meta_description'] %}

Add any custom metadata keys to this array to hide them from the metadata table while keeping them available in Stripe.


πŸ—οΈ Production Deployment

This project includes a compose.prod.yaml for simulating the production build locally.

  1. Set APP_ENV=prod and a secure APP_SECRET.
  2. Use real production Stripe keys.
  3. Configure database persistence (see Database Persistence above).
  4. Configure a transactional email provider (see Email above).
  5. Build for production locally:
    docker compose -f compose.yaml -f compose.prod.yaml build

πŸ“„ License

This project is licensed under the Apache 2.0 License