A webhook-driven notification service that sends enriched payment and subscription events from Shopify and Chargify to Slack. The service provides meaningful, actionable insights for customer success teams with a fun and engaging tone.
- 🎯 Smart Event Processing: Automatically categorizes and prioritizes events based on type and customer value
- 💡 Rich Context: Enriches notifications with customer history, metrics, and actionable insights
- 🎨 Engaging Messages: Uses whimsical language and emojis to make notifications fun and memorable
- 📊 Business Metrics: Includes relevant business metrics like lifetime value and payment history
- 🔄 Event Analysis: Analyzes events to provide actionable recommendations
- ⚡ Multiple Payment Providers: Supports Shopify, Chargify, and Stripe webhooks
- 🔗 One-Click Integrations: OAuth-based connections for Slack and Stripe (no manual token copying)
- Backend: Django 5.x, Python 3.12+
- Database: PostgreSQL 17
- Cache: Redis 7
- Package Manager: uv (fast Python package manager from Astral)
- Linting/Formatting: ruff
- Testing: pytest with pytest-django
- Containerization: Docker with docker-compose
- Clone the repository
git clone [email protected]:notipus/notipus.git
cd notipus- Build the Docker images
docker-compose build- Set environment variables
The application reads configuration from environment variables. Set these before running:
export SECRET_DJANGO_KEY=your-secure-secret-key-here
export DEBUG=True
# Notipus billing (for subscription revenue)
export NOTIPUS_STRIPE_SECRET_KEY=sk_test_your_stripe_key
export NOTIPUS_STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
# Optional: Override other settings
export DB_PASSWORD=secure_db_passwordNote: Webhook secrets for customer integrations are managed per-tenant through the web interface.
- Run the containers
docker-compose up -d- Verify the setup
docker-compose logs -fAccess the application at http://localhost:8000.
- Stop the containers
docker-compose down- Clone the repository
git clone [email protected]:Viktopia/notipus.git
cd notipus- Install uv (Python package manager)
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or with Homebrew
brew install uv- Install dependencies
uv sync --all-groups- Set environment variables
export SECRET_DJANGO_KEY=your-secure-secret-key-here
export DEBUG=True
export DB_NAME=notipus_dev
export DB_USER=postgres
export DB_PASSWORD=postgres
export DB_HOST=localhost- Start PostgreSQL and Redis (using Docker)
docker-compose up -d db redis- Run migrations
uv run python app/manage.py migrate- Start the development server
uv run python app/manage.py runserver# Install all dependencies (including dev tools)
uv sync --all-groups
# Update all dependencies to latest versions
uv lock --upgrade && uv sync --all-groups# Run all tests
uv run pytest
# Run with coverage report
uv run pytest --cov=app --cov-report=term-missing
# Run a specific test file
uv run pytest tests/test_webhooks.py
# Run tests matching a pattern
uv run pytest -k "test_shopify"# Format code
uv run ruff format .
# Run linter
uv run ruff check .
# Auto-fix linting issues
uv run ruff check --fix .
# Type checking
uv run mypy app/# Install pre-commit hooks
uv run pre-commit install
# Run hooks manually on all files
uv run pre-commit run --all-files# Create a superuser
uv run python app/manage.py createsuperuser
# Make migrations (requires PostgreSQL running)
uv run python app/manage.py makemigrations
# Make migrations using SQLite (no PostgreSQL needed)
PYTHONPATH=app DJANGO_SETTINGS_MODULE=django_notipus.test_settings uv run python app/manage.py makemigrations core --name your_migration_name
# Run migrations
uv run python app/manage.py migrate
# Collect static files
uv run python app/manage.py collectstaticThe setup_stripe_plans management command creates Products and Prices in Stripe for your subscription plans, then updates the database with the actual Stripe Price IDs.
# Preview what will be created (dry-run mode)
uv run python app/manage.py setup_stripe_plans --dry-run
# Create all plans in Stripe
uv run python app/manage.py setup_stripe_plans
# Sync a specific plan only
uv run python app/manage.py setup_stripe_plans --plan basic
# Force recreate prices (useful when updating pricing)
uv run python app/manage.py setup_stripe_plans --forceWhat it does:
- Creates a Stripe Product for each paid plan with metadata (plan name, limits, features)
- Creates monthly and yearly recurring Prices with lookup keys (
{plan}_monthly,{plan}_yearly) - Updates the
Planmodel with the returned Stripe Price IDs - Skips plans that already have valid Stripe prices (idempotent by default)
- Reuses existing Products if found by metadata
Options:
| Option | Description |
|---|---|
--dry-run |
Show what would be created without making changes |
--force |
Recreate prices even if they already exist |
--plan NAME |
Only sync a specific plan (e.g., basic, pro, enterprise) |
Note: Free and trial plans (price = $0) are automatically skipped.
The service is built with a modular, multi-tenant architecture that separates concerns and makes it easy to extend:
app/
├── core/ # Core application (users, workspaces, billing)
│ ├── models.py # Workspace, Integration, Company, WorkspaceMember, Plan, etc.
│ ├── services/
│ │ ├── stripe.py # Stripe billing API client
│ │ ├── shopify.py # Shopify Admin API client
│ │ ├── enrichment.py # Domain enrichment orchestration
│ │ ├── dashboard.py # Dashboard data aggregation
│ │ └── webauthn.py # Passwordless authentication
│ └── views/
│ └── integrations/ # OAuth integration handlers (Slack, Stripe, Shopify)
├── plugins/ # Unified plugin architecture (see ADR-001)
│ ├── base.py # BasePlugin, PluginMetadata, PluginType
│ ├── registry.py # PluginRegistry singleton with auto-discovery
│ ├── enrichment/ # Data enrichment plugins
│ │ ├── base.py # BaseEnrichmentPlugin
│ │ └── brandfetch.py # Domain/brand enrichment via Brandfetch API
│ ├── sources/ # Webhook source plugins (payment providers)
│ │ ├── base.py # BaseSourcePlugin
│ │ ├── stripe.py # Stripe webhook processor
│ │ ├── shopify.py # Shopify webhook processor
│ │ └── chargify.py # Chargify/Maxio webhook processor
│ └── destinations/ # Notification destination plugins
│ ├── base.py # BaseDestinationPlugin
│ └── slack.py # Slack Block Kit formatter
├── webhooks/ # Webhook routing and processing
│ ├── webhook_router.py # Multi-tenant webhook routing
│ ├── services/
│ │ ├── event_processor.py # Event processing orchestration
│ │ ├── notification_builder.py # RichNotification construction
│ │ ├── insight_detector.py # Business insight detection
│ │ ├── slack_client.py # Slack API client
│ │ ├── billing.py # Notipus subscription billing handler
│ │ ├── rate_limiter.py # Rate limiting with circuit breaker
│ │ └── database_lookup.py # Cross-reference lookups (Redis)
│ └── models/
│ ├── notification.py # Legacy Notification model
│ └── rich_notification.py # RichNotification with sections/insights
└── django_notipus/ # Django project settings
Notipus is designed as a multi-tenant SaaS platform where each workspace manages their own integrations:
- Workspaces: Each tenant (workspace) has their own webhook endpoints, integrations, and Slack configurations
- Webhook Endpoints: Workspace-specific webhooks at
/webhook/customer/{org_uuid}/{provider}/ - Integration Storage: Credentials and settings stored per-workspace in the
Integrationmodel - Isolation: Each workspace's data is isolated and rate-limited independently
Notipus uses a unified plugin system (documented in ADR-001) that provides a consistent pattern for extending the platform:
Plugin Types:
| Type | Purpose | Examples |
|---|---|---|
| Sources | Receive and validate webhooks from payment providers | Stripe, Shopify, Chargify |
| Destinations | Format and deliver notifications | Slack (with Block Kit) |
| Enrichment | Enhance data with external information | Brandfetch (company logos/branding) |
Key Features:
- Auto-Discovery: Plugins are automatically discovered from
app/plugins/subdirectories - Unified Registry: Single
PluginRegistrysingleton manages all plugin types - Consistent Configuration: All plugins configured via the
PLUGINSDjango setting - Type Safety: Base classes with abstract methods ensure consistent plugin contracts
Plugin Configuration Example:
# In settings.py
PLUGINS = {
"enrichment": {
"brandfetch": {
"enabled": True,
"priority": 100,
"config": {"api_key": "...", "timeout": 10}
}
},
"sources": {
"stripe": {"enabled": True},
"shopify": {"enabled": True},
"chargify": {"enabled": True},
},
"destinations": {
"slack": {"enabled": True},
},
}Notipus supports multiple authentication methods:
- Slack OAuth (Sign in with Slack): Primary authentication method using OpenID Connect
- WebAuthn/Passkeys: Passwordless authentication for enhanced security
- Django Sessions: Traditional session-based auth for API access
To set up the Slack integration, create a Slack app at api.slack.com/apps with the following App Manifest:
{
"display_information": {
"name": "Notipus",
"description": "Smart webhook notifications for payments and subscriptions",
"background_color": "#4A154B"
},
"features": {
"bot_user": {
"display_name": "Notipus",
"always_online": true
}
},
"oauth_config": {
"redirect_urls": [
"https://your-domain.com/accounts/slack/login/callback/",
"https://your-domain.com/api/connect/slack/callback/"
],
"scopes": {
"bot": [
"incoming-webhook",
"chat:write",
"channels:read"
]
}
},
"settings": {
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}Required environment variables for Slack:
SLACK_CLIENT_ID: OAuth client ID from your Slack appSLACK_CLIENT_SECRET: OAuth client secret from your Slack appSLACK_REDIRECT_URI: Login callback URL (e.g.,https://your-domain.com/accounts/slack/login/callback/)SLACK_CONNECT_REDIRECT_URI: Workspace connect callback URL (e.g.,https://your-domain.com/api/connect/slack/callback/)
Scope explanations:
| Scope | Purpose |
|---|---|
incoming-webhook |
Send notifications via webhook URL to a selected channel |
chat:write |
Post messages to channels the bot is a member of |
channels:read |
List public channels so users can select notification destinations |
To enable customers to connect their Stripe accounts with one click (automatic webhook setup), configure Stripe Connect:
- Enable Stripe Connect in your Stripe Dashboard
- Enable OAuth at Connect OAuth settings
- Copy your
client_idfrom the OAuth settings (starts withca_) - Add redirect URI:
https://your-domain.com/api/connect/stripe/callback/ - Choose Standard accounts connection type
Required environment variables for Stripe Connect:
STRIPE_CONNECT_CLIENT_ID: Platform client ID from Connect settings (starts withca_)STRIPE_CONNECT_REDIRECT_URI: OAuth callback URL (e.g.,https://your-domain.com/api/connect/stripe/callback/)BASE_URL: Base URL of your application (e.g.,https://your-domain.com)
How it works:
- User clicks "Connect Stripe" on the integrations page
- User is redirected to Stripe and authorizes the connection
- Notipus automatically creates a webhook endpoint on the user's Stripe account
- User is redirected back with everything configured - no manual setup required
Permissions requested:
| Scope | Purpose |
|---|---|
read_write |
Create webhook endpoints on connected accounts |
To enable Shopify OAuth integration, you need to create a Shopify app in the Shopify Partner Dashboard:
- Create a new app in the Partner Dashboard
- Configure App URLs:
- App URL:
https://your-domain.com - Allowed redirection URLs:
https://your-domain.com/api/connect/shopify/callback/
- App URL:
- Copy credentials from the app's API credentials page
Required environment variables:
| Variable | Description |
|---|---|
SHOPIFY_CLIENT_ID |
Client ID from Shopify Partner Dashboard |
SHOPIFY_CLIENT_SECRET |
Client secret from Shopify Partner Dashboard |
SHOPIFY_REDIRECT_URI |
OAuth callback URL (e.g., https://your-domain.com/api/connect/shopify/callback/) |
Shopify CLI Configuration:
The app uses shopify.app.toml for Shopify CLI configuration. Copy the example file and configure it:
cp shopify.app.toml.example shopify.app.toml
# Edit shopify.app.toml with your client_id and URLsThen deploy your app configuration to sync with Shopify:
shopify app deployOAuth Scopes:
| Scope | Purpose |
|---|---|
read_orders |
Subscribe to order webhooks (orders/create, orders/paid, etc.) |
read_customers |
Subscribe to customer webhooks (customers/create, customers/update, etc.) |
Note: There is no
write_webhooksscope in Shopify. Webhook creation permissions are controlled by having the appropriate read scope for the resource you want to subscribe to.
The Brandfetch integration enriches company domains with brand information:
- Automatically fetches company logos, colors, and descriptions
- Caches results in the
Companymodel to reduce API calls - Used to enhance Slack notifications with company branding
The webhook system includes robust rate limiting:
- Per-Workspace Limits: Configurable limits based on subscription plan
- Redis-Backed: Uses Redis for distributed rate limit tracking
- Circuit Breaker: Automatically disables failing integrations to prevent cascading failures
- Graceful Degradation: Falls back to in-memory counting if Redis is unavailable
Redis is used for multiple purposes:
- Rate Limit Tracking: Per-workspace request counts with TTL
- Recent Activity: Last 7 days of webhook activity for dashboard display
- Session Cache: Django session storage (configurable)
- Circuit Breaker State: Tracks integration health status
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Install dependencies:
uv sync --all-groups - Make your changes and ensure tests pass:
uv run pytest - Format and lint:
uv run ruff format . && uv run ruff check . - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a pull request
Required for production:
SECRET_DJANGO_KEY: Django secret key for cryptographic operationsDB_PASSWORD: Database password for PostgreSQL
Notipus billing (subscription revenue):
STRIPE_SECRET_KEY: Stripe secret key for processing subscriptionsSTRIPE_PUBLISHABLE_KEY: Stripe publishable key for frontend integrationSTRIPE_API_VERSION: Stripe API version (default:2025-12-15.clover)NOTIPUS_STRIPE_WEBHOOK_SECRET: Webhook secret for Stripe billing events (from Stripe Dashboard → Webhooks)
Important: Set
NOTIPUS_STRIPE_WEBHOOK_SECRETbefore running migrations. The migration0017_add_stripe_billing_integrationcreates the requiredGlobalBillingIntegrationrecord using this environment variable.
Stripe Checkout and Portal URLs:
STRIPE_SUCCESS_URL: Redirect URL after successful checkout (default:http://localhost:8000/billing/checkout/success/)STRIPE_CANCEL_URL: Redirect URL after cancelled checkout (default:http://localhost:8000/billing/checkout/cancel/)STRIPE_PORTAL_RETURN_URL: Return URL from Customer Portal (default:http://localhost:8000/billing/)
Setting up Stripe Plans:
After configuring your Stripe keys, run the setup command to create Products and Prices in Stripe:
uv run python app/manage.py setup_stripe_plansThis creates Stripe Products/Prices for your plans and updates the database with the Price IDs. See Stripe Plan Setup for more details.
Legacy plan ID mapping (optional, prefer using the setup command above):
STRIPE_BASIC_PLAN: Stripe price ID for basic planSTRIPE_PRO_PLAN: Stripe price ID for pro planSTRIPE_ENTERPRISE_PLAN: Stripe price ID for enterprise plan
Stripe Connect OAuth (for customer integrations):
STRIPE_CONNECT_CLIENT_ID: Platform client ID from Stripe Connect settings (starts withca_)STRIPE_CONNECT_REDIRECT_URI: OAuth callback URL (default:http://localhost:8000/api/connect/stripe/callback/)BASE_URL: Base URL for webhook endpoints (default:http://localhost:8000)
Optional overrides:
DEBUG: Set to "False" for production (defaults to "True")ALLOWED_HOSTS: Comma-separated list of allowed hostnamesDB_NAME,DB_USER,DB_HOST,DB_PORT: Database connection parametersREDIS_URL: Redis connection URL
Customer webhook integrations are configured per-tenant through the web interface. Each workspace manages their own:
- Stripe: One-click OAuth connection (automatic webhook setup)
- Slack: One-click OAuth connection for notifications
- Shopify: OAuth connection with automatic webhook subscription setup
- Chargify: Webhook secret configuration
Customer Webhooks (per-workspace):
POST /webhook/customer/{org_uuid}/shopify/- Shopify order and customer eventsPOST /webhook/customer/{org_uuid}/chargify/- Chargify/Maxio subscription eventsPOST /webhook/customer/{org_uuid}/stripe/- Stripe payment events
Global Webhooks (Notipus billing):
POST /webhook/billing/stripe/- Notipus subscription billing events
Important: The billing webhook requires a
GlobalBillingIntegrationdatabase record. This is created automatically by running migrations. EnsureNOTIPUS_STRIPE_WEBHOOK_SECRETis set in your environment before runningpython manage.py migrate.
Shopify:
orders/paid- New order paymentscustomers/create,customers/update- Customer lifecycle events
Chargify/Maxio:
payment_success,payment_failure- Payment outcomessubscription_created,subscription_updated- Subscription lifecyclerenewal_success,renewal_failure- Renewal events
Stripe:
customer.subscription.created- New subscription createdcustomer.subscription.trial_will_end- Trial ending notification (3 days before)invoice.payment_succeeded- Invoice payment successfulinvoice.payment_failed- Invoice payment failedinvoice.paid- Invoice paid confirmationinvoice.payment_action_required- Payment requires customer action (3DS)checkout.session.completed- Checkout completion
- HMAC Signature Validation: All webhooks require valid signatures
- Timestamp Validation: Prevents replay attacks (Chargify)
- Production-Only Enforcement: Webhook validation cannot be bypassed in production
- Request Timeouts: All external API calls have configured timeouts