This document describes the current jobs setup using:
- PostgreSQL +
pgpm-database-jobs(app_jobs.*) @constructive-io/knative-job-service+@constructive-io/knative-job-worker- Knative functions (example:
send-email)
Jobs live entirely in Postgres, provided by the pgpm-database-jobs extension.
Key pieces:
- Schema:
app_jobs - Tables:
app_jobs.jobs– queued / running jobsapp_jobs.scheduled_jobs– cron-like scheduled jobs
- Functions:
app_jobs.add_job(...)app_jobs.add_scheduled_job(...)app_jobs.get_job(...)app_jobs.get_scheduled_job(...)app_jobs.complete_job(...)app_jobs.fail_job(...)app_jobs.run_scheduled_job(...)
Install the extension into your app database (the same DB your API uses). In SQL:
CREATE EXTENSION IF NOT EXISTS pgpm-database-jobs;Once installed you should see:
\dt app_jobs.*and at least app_jobs.jobs and app_jobs.scheduled_jobs present.
The jobs runtime consists of:
@constructive-io/knative-job-service- Starts:
- an HTTP callback server (
@constructive-io/knative-job-server) - a Knative job worker (
@constructive-io/knative-job-worker) - a scheduler (
@constructive-io/job-scheduler)
- an HTTP callback server (
- Starts:
@constructive-io/knative-job-worker- Polls
app_jobs.jobsfor work - For each job,
POSTs to${KNATIVE_SERVICE_URL}/${task_identifier} - Uses
X-Worker-Id,X-Job-Id,X-Database-Id,X-Actor-Idheaders and JSON payload
- Polls
From jobs/knative-job-service/src/env.ts:
-
Postgres
PGUSER– DB userPGHOST– DB hostPGPASSWORD– DB passwordPGPORT– DB port (default5432)PGDATABASE– the app DB that haspgpm-database-jobsinstalledJOBS_SCHEMA– schema for jobs (defaultapp_jobs)
-
Worker configuration
JOBS_SUPPORT_ANY–trueto accept all tasks,falseto restrictJOBS_SUPPORTED– comma-separated list of task names ifJOBS_SUPPORT_ANY=falseHOSTNAME– worker/scheduler ID (used in logs and job-utils)
-
Callback server
INTERNAL_JOBS_CALLBACK_PORT– port to bind the callback HTTP server (default12345)INTERNAL_JOBS_CALLBACK_URL– full URL to that server, e.g.
http://knative-job-service.interweb.svc.cluster.local:8080
-
Function gateway
KNATIVE_SERVICE_URL– base URL for Knative functions, e.g.
http://send-email.interweb.svc.cluster.localINTERNAL_GATEWAY_URL– fallback used by the worker; set this equal toKNATIVE_SERVICE_URLto keep env validation happy
The functions/send-verification-link package is a Knative function that sends verification links for:
- invite_email - User invitations
- forgot_password - Password reset emails
- email_verification - Email verification links
- Receives job payload with email type and parameters
- Queries GraphQL API (via
private.localhosthost routing) for:GetDatabaseInfo- Site configuration (domains, logo, theme, legal terms)GetUser- Sender info for invite emails
- Generates HTML email using MJML templates
- Sends via Mailgun (or logs in dry-run mode)
# GraphQL endpoints (admin server with host-based routing)
GRAPHQL_URL: "http://constructive-admin-server:3000/graphql"
META_GRAPHQL_URL: "http://constructive-admin-server:3000/graphql"
GRAPHQL_HOST_HEADER: "private.localhost"
META_GRAPHQL_HOST_HEADER: "private.localhost"
# Mailgun configuration
MAILGUN_API_KEY: "your-api-key"
MAILGUN_DOMAIN: "mg.example.com"
MAILGUN_FROM: "[email protected]"
MAILGUN_REPLY: "[email protected]"
# Dry run mode (no actual emails sent)
SEND_VERIFICATION_LINK_DRY_RUN: "true"# Start postgres and minio first
docker-compose up -d
# Start the jobs services
docker-compose -f docker-compose.jobs.yml up --build| Service | Port | Description |
|---|---|---|
constructive-admin-server |
3001 | GraphQL API with API_IS_PUBLIC=false |
send-verification-link |
8082 | Verification link function |
knative-job-service |
8080 | Job worker + callback server |
# Introspect the private API
curl -X POST http://localhost:3001/graphql \
-H "Content-Type: application/json" \
-H "Host: private.localhost" \
-d '{"query": "{ __schema { queryType { fields { name } } } }"}'
# List databases
curl -X POST http://localhost:3001/graphql \
-H "Content-Type: application/json" \
-H "Host: private.localhost" \
-d '{"query": "{ databases { nodes { id name } } }"}'
# List users
curl -X POST http://localhost:3001/graphql \
-H "Content-Type: application/json" \
-H "Host: private.localhost" \
-d '{"query": "{ users { nodes { id username displayName } } }"}'# Get Database ID
DBID="$(docker exec -i postgres psql -U postgres -d constructive -Atc \
'SELECT id FROM metaschema_public.database ORDER BY created_at LIMIT 1;')"
echo "Database ID: $DBID"
# Get User ID (for sender_id in invite emails)
SENDER_ID="$(docker exec -i postgres psql -U postgres -d constructive -Atc \
'SELECT id FROM roles_public.users ORDER BY created_at LIMIT 1;')"
echo "Sender ID: $SENDER_ID"# Set JWT claims so add_job can read database_id and actor_id internally
docker exec -it postgres \
psql -U postgres -d constructive -c "
SELECT set_config('jwt.claims.database_id', '$DBID', true);
SELECT set_config('jwt.claims.user_id', '$SENDER_ID', true);
SELECT app_jobs.add_job(
'email:send_verification_link',
json_build_object(
'email_type', 'invite_email',
'email', '[email protected]',
'invite_token', 'invite-token-123',
'sender_id', '$SENDER_ID'
)::json
);
"docker exec -it postgres \
psql -U postgres -d constructive -c "
SELECT set_config('jwt.claims.database_id', '$DBID', true);
SELECT app_jobs.add_job(
'email:send_verification_link',
json_build_object(
'email_type', 'forgot_password',
'email', '[email protected]',
'user_id', '$SENDER_ID',
'reset_token', 'reset-token-123'
)::json
);
"docker exec -it postgres \
psql -U postgres -d constructive -c "
SELECT set_config('jwt.claims.database_id', '$DBID', true);
SELECT app_jobs.add_job(
'email:send_verification_link',
json_build_object(
'email_type', 'email_verification',
'email', '[email protected]',
'email_id', '$(uuidgen)',
'verification_token', 'verify-token-123'
)::json
);
"# Watch send-verification-link function logs
docker logs -f send-verification-link
# Watch job service logs
docker logs -f knative-job-serviceapp_jobs.add_jobinserts intoapp_jobs.jobsand firesNOTIFY "jobs:insert"knative-job-workerreceives notification, picks up the job- Worker
POSTs payload tohttp://send-verification-link:8080/ send-verification-linkqueries GraphQL for site/user info- Generates email HTML and sends (or logs in dry-run mode)
- Returns
{ complete: true }and job is marked complete
You can inspect the queue directly:
SELECT
id,
task_identifier,
attempts,
max_attempts,
last_error,
locked_by,
locked_at,
run_at,
created_at,
updated_at
FROM app_jobs.jobs
ORDER BY id DESC;Completed jobs are removed from app_jobs.jobs by the completion logic; failed jobs with retries will show a last_error and incremented attempts.
You can also use app_jobs.scheduled_jobs and @constructive-io/job-scheduler to run recurring jobs.
Example (generic, not specific to send-email):
-- database_id and actor_id are read from JWT claims automatically
SELECT app_jobs.add_scheduled_job(
identifier := 'some-task-name',
payload := json_build_object('foo', 'bar'),
schedule_info := json_build_object(
'start', NOW(),
'end', NOW() + '1 day'::interval,
'rule', '*/5 * * * *' -- every 5 minutes (cron rule)
)
);The scheduler will:
- Read from
app_jobs.scheduled_jobs. - Use
app_jobs.run_scheduled_jobto materialize real jobs intoapp_jobs.jobs. - The worker then processes them like any other job.
Inspect scheduled jobs:
SELECT
id,
task_identifier,
payload,
schedule_info,
last_scheduled,
last_scheduled_id
FROM app_jobs.scheduled_jobs
ORDER BY id DESC;