Backend

Installation

From an empty folder to a Next.js app talking to Django: Python package, settings, URLs, services, workers and the client.

You need Python 3.12 or newer, Node 20 or newer, and Docker for the development services. The steps below create a Django project next to a Next.js app; apps/backend-demo in the repository is the finished result if you prefer to start from it.

Backend

  1. Start MongoDB, Redis and Mailpit

    The repository ships a docker-compose.yml with a single node MongoDB replica set (transactions need a replica set), Redis and Mailpit, which catches every email and shows it at http://localhost:8025.

    sh
    docker compose up -d
    ServicePortUsed for
    mongo27027Database, sessions, GridFS files
    redis6389Cache, rate limits, Celery broker, realtime events
    mailpit1025, 8025SMTP server and web inbox
  2. Install the package

    The base install covers accounts, 2FA, OAuth, roles, API keys, files on GridFS, notifications, emails over SMTP, audit, rate limits, webhooks and marketing. Extras add the integrations; all takes every one.

    sh
    uv add "pleate-backend[all]"
    # or
    pip install "pleate-backend[all]"
    ExtraAddsFor
    s3django-storages[s3]Files on S3 or compatible storage, presigned uploads
    stripestripeBilling
    anthropicanthropicAI chat with Claude models
    openaiopenaiAI chat with OpenAI and compatible servers (LiteLLM, vLLM, Ollama)
    pushpywebpushWeb Push notifications
    anymaildjango-anymailResend, Postmark, SES and other email APIs
  3. Import the settings

    pleate_backend.settings holds a complete configuration: installed apps, middleware, DRF, sessions, password hashing, logging and Celery. Import it, then set what belongs to your deployment. Every value can be overridden after the import.

    myproject/settings.py
    import os
    from django.core.exceptions import ImproperlyConfigured
    from pleate_backend.settings import *  # noqa: F403
    from pleate_backend.settings import INSTALLED_APPS, database, redis
    
    env = os.environ.get
    
    DEBUG = env("DEBUG", "0") == "1"                # DEBUG=1 for local development
    SECRET_KEY = env("SECRET_KEY") or ("dev-only-change-me" if DEBUG else None)
    if not SECRET_KEY:
        raise ImproperlyConfigured("Set SECRET_KEY")
    ALLOWED_HOSTS = env("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
    CSRF_TRUSTED_ORIGINS = env("CSRF_TRUSTED_ORIGINS", "http://localhost:3000").split(",")
    ROOT_URLCONF = "myproject.urls"
    
    DATABASES = database(env("MONGODB_URI", "mongodb://localhost:27027/myapp?directConnection=true"))
    INSTALLED_APPS = [*INSTALLED_APPS, "catalog"]   # your own apps after pleate's
    
    REDIS_URL = env("REDIS_URL")                    # redis://localhost:6389/0
    if REDIS_URL:
        globals().update(redis(REDIS_URL))          # cache, rate limits and Celery on Redis
    
    MAILERS = {
        "default": {
            "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
            "OPTIONS": {"host": "localhost", "port": 1025},   # Mailpit
        }
    }
    DEFAULT_FROM_EMAIL = "Plate <hello@plate.test>"
    
    # The base settings make both cookies secure; plain http needs them off.
    SESSION_COOKIE_SECURE = not DEBUG
    CSRF_COOKIE_SECURE = not DEBUG
    
    PLEATE = {
        "SITE_NAME": "Plate",
        "FRONTEND_URL": env("FRONTEND_URL", "http://localhost:3000"),
        "REDIS_URL": REDIS_URL,
        "REALTIME": {"BACKEND": "pleate_backend.core.realtime.RedisBroker" if REDIS_URL
                     else "pleate_backend.core.realtime.MemoryBroker"},
        "EMAILS": {"ASYNC": bool(REDIS_URL)},       # send inline when there is no worker
    }

    Only the apps you list are mounted. To leave one out, build the list yourself from DJANGO_APPS and PLEATE_APPS:

    python
    from pleate_backend.settings import DJANGO_APPS, PLEATE_APPS
    
    INSTALLED_APPS = [
        *DJANGO_APPS,
        *[app for app in PLEATE_APPS if app != "pleate_backend.billing"],
        "catalog",
    ]
  4. Mount the URLs

    One include mounts every installed app under its prefix, the OpenAPI schema at /api/schema and a Swagger UI at /api/docs. Your own resources go under /api too, so the client reaches them with the same cookies and conventions.

    myproject/urls.py
    from django.contrib import admin
    from django.urls import include, path
    
    urlpatterns = [
        path("admin/", admin.site.urls),
        path("api/", include("pleate_backend.urls")),
        path("api/catalog/", include("catalog.urls")),
    ]
  5. Create the collections and a staff user

    sh
    python manage.py migrate
    python manage.py createsuperuser
    python manage.py runserver 8000

    Open http://localhost:8000/api/health: it answers {"status": "ok", "checks": {"mongodb": "ok", "redis": "ok"}} when both services are reachable, and 503 with degraded otherwise.

  6. Run the worker

    Emails, webhook deliveries and housekeeping run on Celery. Add the usual celery.py and start one worker with the beat scheduler embedded. Without Redis, set PLEATE["EMAILS"]["ASYNC"] = False and emails are sent inline.

    myproject/celery.py
    import os
    from celery import Celery
    
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
    
    app = Celery("myproject")
    app.config_from_object("django.conf:settings", namespace="CELERY")
    app.autodiscover_tasks()
    sh
    celery -A myproject worker -B -l info

Frontend

  1. Install the client

    The hooks use TanStack Query, a peer dependency. pleate provides the components the hooks feed.

    sh
    npm install pleate pleate-client @tanstack/react-query
  2. Proxy /api to Django

    Rewrite /api/* to Django so that the browser sees one origin. The session and CSRF cookies are then first party, SameSite=Lax works, and there is no CORS to configure.

    next.config.ts
    import type { NextConfig } from "next";
    
    const API = process.env.PLEATE_API_ORIGIN ?? "http://localhost:8000";
    
    const config: NextConfig = {
      async rewrites() {
        return [{ source: "/api/:path*", destination: `${API}/api/:path*` }];
      },
    };
    
    export default config;
  3. Add the provider

    PleateProvider creates the client and a QueryClient (or takes yours) and makes them available to every hook. It is a client component: put it in a providers file and wrap the root layout with it.

    app/providers.tsx
    "use client";
    import { PleateProvider } from "pleate-client/react";
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <PleateProvider
          options={{
            baseUrl: "/api",
            onUnauthorized: () => location.assign("/login"),
          }}
        >
          {children}
        </PleateProvider>
      );
    }
    app/layout.tsx
    import { Providers } from "./providers";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            <Providers>{children}</Providers>
          </body>
        </html>
      );
    }
  4. Use a hook

    tsx
    "use client";
    import { useMe } from "pleate-client/react";
    
    export function Greeting() {
      const { user, isPending } = useMe();
      if (isPending) return null;
      return <p>{user ? `Hello, ${user.display_name}` : "Signed out"}</p>;
    }

Types from the schema

Every response type in pleate-client is generated from the backend's OpenAPI document, so the client always matches the server. Regenerate it after adding fields with npm run schema in packages/pleate-client, or point openapi-typescript at /api/schema of your own project to type your resources the same way.