SES Tracking is a self-hosted Amazon SES (Simple Email Service) analytics application built with Laravel. It ingests SES event notifications through Amazon SNS webhooks and turns them into dashboards, reports, and exports so you can monitor email delivery, bounces, complaints, opens, clicks, and related metrics across teams and projects.
Website: sestracking.com
- Teams sending mail through AWS SES who want private, self-hosted visibility into the full event stream—not only sends, but per-recipient outcomes.
- Operators who need multi-project isolation (separate webhook URLs and access) and multi-user admin vs regular roles.
- Anyone migrating from or comparing to sesdashboard and wanting a Laravel 11 stack, tests, optional webhook payload logging, and robust handling of edge cases (for example open and click attribution when one message has multiple recipients).
This project takes structural inspiration from SES Dashboard (MIT, Copyright (c) 2020 Nikeev) but is maintained separately: different codebase, Laravel 11, automated tests, built-in debug logging for webhooks (toggle via env), multi-user and multi-project support, and corrected SES event handling for tricky real-world cases.
License: MIT
Suggested GitHub topics: aws-ses, amazon-ses, sns, email-tracking, email-analytics, webhooks, laravel, php, self-hosted, transactional-email, dashboard
- Multi-user support: Administrator and regular user roles
- Multi-project management: Dedicated webhook endpoints per project (
/webhook/{project_token}) - Email event tracking: Delivery, bounces, complaints, opens, clicks, delays (as surfaced by SES/SNS)
- AWS integration: SNS → HTTPS webhook processing with idempotent handling
- Analytics dashboard: Chart.js charts, filters by project and date range
- Data export: CSV and Excel with filters
- Responsive UI: Bootstrap 5 frontend (including Vue components where used), compiled assets included in the repo
Overview of send, delivery, opens, clicks, and not-delivered metrics with project and date filters, plus a multi-series trend chart (send, delivery, bounce, complaint, delivery delay, open, click).
- Backend: Laravel 11, PHP 8.2+, MySQL 8
- Frontend: JavaScript with Bootstrap 5 and Chart.js
- Build: Vite (via
laravel-vite-plugin) - Email provider: Amazon SES
- Ingest: Amazon SNS notifications → Laravel webhook controllers (idempotent processing)
- PHP 8.2+
- MySQL 8.0+
- Composer
- Node.js 18+ and npm (to build the frontend assets)
Note: Compiled frontend assets are not committed to git. Run
npm install && npm run buildon every install and deploy; the built files go topublic/build/.
-
Clone and install dependencies:
git clone https://github.com/alephcom/sestracking.git cd sestracking composer install npm install npm run build -
Environment configuration:
cp .env.example .env php artisan key:generate
-
Database setup:
php artisan migrate php artisan db:seed
-
Start the development server:
php artisan serve
-
Start the Vite dev server (hot reloads styles and scripts as you edit them):
npm run dev
Keep it running alongside
php artisan serve. -
Build for production:
npm run build
Upgrading from an older version? Assets used to be committed under
public/js,public/cssandpublic/fontsand built with Laravel Mix. Those folders are no longer used; runnpm install && npm run buildas part of your deploy, and delete the old folders if they are left over.
The database seeder creates an initial admin user:
- Email:
admin@example.com - Password:
password - Role: Administrator
This user has full access to all features including user management and project administration.
Update the following parameters in your .env file:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database_name
DB_USERNAME=your_username
DB_PASSWORD=your_passwordLaravel’s SES mail transport (send test) uses AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_DEFAULT_REGION in .env as usual.
SES account-level suppression list features (admin UI list/add/remove, webhook auto-push, and the scheduled daily import into ses_suppressed_destinations) use only the per-project Access Key ID, Secret Access Key, and region fields on each project in the admin UI. Global .env credentials are not used for those APIs.
Required IAM actions for suppression are listed on each project’s edit page and in config/ses_suppression.php (ses:PutSuppressedDestination, ses:ListSuppressedDestinations, ses:DeleteSuppressedDestination; ses:GetSuppressedDestination is available on the service for future use). Use "Resource": "*" in the policy unless your organization scopes further. The project’s AWS region must match the region where you send with SES for that account.
A queued job runs daily (see routes/console.php) to copy the account suppression list into ses_suppressed_destinations for each project that has per-project SES keys and a resolvable region. Ensure php artisan schedule:run is on your server crontab so the scheduler fires.
If your SES account already adds recipients to the account suppression list automatically for bounces or complaints, enabling auto-push in this app can be redundant but is otherwise safe.
AWS_ACCESS_KEY_ID=your_aws_access_key
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
AWS_DEFAULT_REGION=your_aws_region
MAIL_MAILER=ses
MAIL_FROM_ADDRESS=your_verified_email@domain.com
MAIL_FROM_NAME="${APP_NAME}"APP_NAME="SES Tracking"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
Dashboard figures are cached for up to 5 minutes. With a cache store that supports tags (Redis or Memcached), each new webhook event clears the cache for its project, so the dashboard shows new events on the next load:
CACHE_STORE=redisWith the default file or database stores the dashboard still works, but can be up to 5 minutes behind.
# Enable webhook payload logging for debugging (disable in production)
WEBHOOK_DEBUG_LOG=falseRun the test suite:
vendor/bin/phpunitUsers who sign in with email and password (not Google/Microsoft SSO) must enroll an authenticator app (TOTP). Recovery codes are shown once after enrollment.
Operator lockouts: Clear 2FA for a user so they can enroll again on next sign-in:
php artisan user:reset-two-factor {user@email.com|user_id}SSO-only users are rejected by that command (they do not use in-app TOTP).
Application key rotation: two_factor_secret is stored encrypted with your APP_KEY. If you run php artisan key:generate or change APP_KEY without decrypting existing data, existing TOTP secrets become unreadable. After a key rotation, affected users cannot sign in with TOTP until an operator runs user:reset-two-factor for each affected account (they set up a new authenticator on next login).
Point Amazon SES event publishing at an Amazon SNS topic, then subscribe your app’s HTTPS endpoint to that topic. Each project exposes a unique path used as the SNS HTTPS subscription URL (confirm the subscription in SNS when prompted).
Configure configuration sets and event types (send, delivery, bounce, complaint, open, click, etc.) in the SES console so SES publishes notifications to SNS; SNS delivers JSON payloads to:
POST https://<your-domain.com>/webhook/{project_token}
Each project has a unique token for webhook authentication.
Every webhook request must be signed by Amazon SNS. The app checks the signature against AWS's signing certificate (downloaded from sns.<region>.amazonaws.com and cached for a day) and rejects anything else with 403, so events can't simply be posted to the URL by hand. Your server needs outbound HTTPS access to sns.*.amazonaws.com.
The signature proves a message came through Amazon SNS, not which SNS topic sent it, so still treat the webhook URL as a secret.
If a project posts SES events to the webhook directly, without SNS, those requests carry no signature. A project admin can allow them under Manage Projects → Edit → Webhook security → Accept webhook requests that are not signed by Amazon SNS. Only turn this on when you need it: anyone who knows that project's webhook URL could then send fake events.
The webhook only checks each notification and puts it on a queue, then answers SNS straight away. A queue worker does the actual saving, so events will not show up on the dashboard unless a worker is running. With the default QUEUE_CONNECTION=database:
php artisan queue:work --tries=3In production, keep it running with a process manager such as Supervisor or systemd, and restart it after each deploy (php artisan queue:restart) so it picks up new code. Jobs that fail every attempt are kept in the failed_jobs table; see them with php artisan queue:failed and retry with php artisan queue:retry all.
For local development you can set QUEUE_CONNECTION=sync to process webhooks immediately without a worker.
- Add more detailed documentation for setting up AWS SES and SNS