Backend
Installation
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
Start MongoDB, Redis and Mailpit
The repository ships a
docker-compose.ymlwith a single node MongoDB replica set (transactions need a replica set), Redis and Mailpit, which catches every email and shows it athttp://localhost:8025.shdocker compose up -dService Port Used for mongo 27027 Database, sessions, GridFS files redis 6389 Cache, rate limits, Celery broker, realtime events mailpit 1025, 8025 SMTP server and web inbox 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;
alltakes every one.shuv add "pleate-backend[all]" # or pip install "pleate-backend[all]"Extra Adds For s3 django-storages[s3] Files on S3 or compatible storage, presigned uploads stripe stripe Billing anthropic anthropic AI chat with Claude models openai openai AI chat with OpenAI and compatible servers (LiteLLM, vLLM, Ollama) push pywebpush Web Push notifications anymail django-anymail Resend, Postmark, SES and other email APIs Import the settings
pleate_backend.settingsholds 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.pyimport 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_APPSandPLEATE_APPS:pythonfrom pleate_backend.settings import DJANGO_APPS, PLEATE_APPS INSTALLED_APPS = [ *DJANGO_APPS, *[app for app in PLEATE_APPS if app != "pleate_backend.billing"], "catalog", ]Mount the URLs
One include mounts every installed app under its prefix, the OpenAPI schema at
/api/schemaand a Swagger UI at/api/docs. Your own resources go under/apitoo, so the client reaches them with the same cookies and conventions.myproject/urls.pyfrom 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")), ]Create the collections and a staff user
shpython manage.py migrate python manage.py createsuperuser python manage.py runserver 8000Open
http://localhost:8000/api/health: it answers{"status": "ok", "checks": {"mongodb": "ok", "redis": "ok"}}when both services are reachable, and 503 withdegradedotherwise.Run the worker
Emails, webhook deliveries and housekeeping run on Celery. Add the usual
celery.pyand start one worker with the beat scheduler embedded. Without Redis, setPLEATE["EMAILS"]["ASYNC"] = Falseand emails are sent inline.myproject/celery.pyimport 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()shcelery -A myproject worker -B -l info
Frontend
Install the client
The hooks use TanStack Query, a peer dependency.
pleateprovides the components the hooks feed.shnpm install pleate pleate-client @tanstack/react-queryProxy /api to Django
Rewrite
/api/*to Django so that the browser sees one origin. The session and CSRF cookies are then first party,SameSite=Laxworks, and there is no CORS to configure.next.config.tsimport 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;Add the provider
PleateProvidercreates 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.tsximport { Providers } from "./providers"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); }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.