Add nix-infra-machine

This commit is contained in:
Ruben Hensen
2026-03-15 18:31:53 +01:00
parent 28ea6f4a47
commit 1034986fa1
79 changed files with 14465 additions and 0 deletions
@@ -0,0 +1,706 @@
{ config, pkgs, lib, ... }:
let
appName = "beiwe-backend";
defaultPort = 8080;
cfg = config.infrastructure.${appName};
# Build the beiwe-backend package
beiwePackage = if cfg.package != null then cfg.package else
pkgs.callPackage ./package.nix {
rev = cfg.version;
};
# Construct the Celery broker URL from RabbitMQ settings
celeryBrokerUrl = if cfg.celery.enable then
"amqp://${cfg.celery.rabbitmq.user}:${cfg.celery.rabbitmq.password}@${cfg.celery.rabbitmq.host}:${toString cfg.celery.rabbitmq.port}/${cfg.celery.rabbitmq.vhost}"
else "";
# Environment variables for beiwe-backend configuration
# See: https://github.com/jhsware/beiwe-backend (fork with env var support)
beiweEnvironment = {
# Required settings
DOMAIN_NAME = cfg.domainName;
FLASK_SECRET_KEY = cfg.flaskSecretKey;
SYSADMIN_EMAILS = cfg.sysadminEmails;
# Database settings (PostgreSQL)
RDS_DB_NAME = cfg.database.name;
RDS_USERNAME = cfg.database.user;
RDS_PASSWORD = cfg.database.password;
RDS_HOSTNAME = cfg.database.host;
RDS_PORT = toString cfg.database.port;
# PostgreSQL SSL mode
# Multiple env vars to ensure compatibility with different Django/psycopg versions
PGSSLMODE = cfg.database.sslmode;
DATABASE_SSLMODE = cfg.database.sslmode;
# S3/MinIO settings
S3_BUCKET = cfg.s3.bucket;
AWS_ACCESS_KEY_ID = cfg.s3.accessKeyId;
AWS_SECRET_ACCESS_KEY = cfg.s3.secretAccessKey;
BEIWE_SERVER_AWS_ACCESS_KEY_ID = cfg.s3.accessKeyId;
BEIWE_SERVER_AWS_SECRET_ACCESS_KEY = cfg.s3.secretAccessKey;
S3_ACCESS_CREDENTIALS_USER = cfg.s3.accessKeyId;
S3_ACCESS_CREDENTIALS_KEY = cfg.s3.secretAccessKey;
# Django settings
DJANGO_SETTINGS_MODULE = "config.django_settings";
} // (lib.optionalAttrs (cfg.s3.endpoint != "") {
# Custom S3 endpoint for MinIO
S3_ENDPOINT_URL = cfg.s3.endpoint;
AWS_S3_ENDPOINT_URL = cfg.s3.endpoint;
}) // (lib.optionalAttrs (cfg.sentry.dsn != "") {
# Sentry error tracking (optional)
SENTRY_ELASTIC_BEANSTALK_DSN = cfg.sentry.dsn;
SENTRY_DATA_PROCESSING_DSN = cfg.sentry.dsn;
}) // (lib.optionalAttrs cfg.celery.enable {
# Celery/RabbitMQ settings
CELERY_BROKER_URL = celeryBrokerUrl;
BROKER_URL = celeryBrokerUrl;
# jhsware fork environment variables for Celery configuration
# These replace the manager_ip file requirement
CELERY_MANAGER_IP = "${cfg.celery.rabbitmq.host}:${toString cfg.celery.rabbitmq.port}";
CELERY_PASSWORD = cfg.celery.rabbitmq.password;
}) // cfg.extraEnvironment;
in
{
options.infrastructure.${appName} = {
enable = lib.mkEnableOption "infrastructure.beiwe-backend";
# ==========================================================================
# Package and Version Configuration
# ==========================================================================
version = lib.mkOption {
type = lib.types.str;
description = ''
Git commit hash of beiwe-backend to install.
Uses jhsware fork which adds environment variable support for Celery.
Supported versions are defined in package.nix.
See package.nix for instructions on adding new versions.
'';
default = "93be878"; # jhsware fork with CELERY_MANAGER_IP/CELERY_PASSWORD env var support
example = "main";
};
package = lib.mkOption {
type = lib.types.nullOr lib.types.package;
description = ''
Custom beiwe-backend package to use. If null, the package will be built
using the version specified in 'version' option.
'';
default = null;
};
# ==========================================================================
# Network Configuration
# ==========================================================================
bindToIp = lib.mkOption {
type = lib.types.str;
description = "IP address to bind beiwe-backend to.";
default = "127.0.0.1";
example = "0.0.0.0";
};
bindToPort = lib.mkOption {
type = lib.types.int;
description = "Port for beiwe-backend web interface.";
default = defaultPort;
};
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Open firewall for beiwe-backend.";
default = false;
};
domainName = lib.mkOption {
type = lib.types.str;
description = "Domain name for the Beiwe backend (used in DOMAIN_NAME env var).";
default = "localhost:8080";
example = "beiwe.example.com";
};
# ==========================================================================
# Security Configuration
# ==========================================================================
flaskSecretKey = lib.mkOption {
type = lib.types.str;
description = ''
A unique, cryptographically secure string for Flask sessions.
IMPORTANT: Change this in production!
'';
default = "CHANGE_ME_IN_PRODUCTION_use_a_random_string";
example = "your-super-secret-random-key-here";
};
sysadminEmails = lib.mkOption {
type = lib.types.str;
description = "System administrator email addresses (comma-separated).";
default = "sysadmin@localhost";
example = "admin@example.com";
};
# ==========================================================================
# Data Directory
# ==========================================================================
dataDir = lib.mkOption {
type = lib.types.path;
description = "Directory where beiwe-backend data is stored.";
default = "/var/lib/beiwe-backend";
};
# ==========================================================================
# Database Configuration (PostgreSQL)
# ==========================================================================
database = {
host = lib.mkOption {
type = lib.types.str;
description = "PostgreSQL host.";
default = "/run/postgresql";
example = "localhost";
};
port = lib.mkOption {
type = lib.types.int;
description = "PostgreSQL port.";
default = 5432;
};
name = lib.mkOption {
type = lib.types.str;
description = "PostgreSQL database name.";
default = "beiwe";
};
user = lib.mkOption {
type = lib.types.str;
description = "PostgreSQL user.";
default = "beiwe";
};
password = lib.mkOption {
type = lib.types.str;
description = ''
PostgreSQL password. Required by Beiwe even when using trust authentication.
For trust authentication, use an empty string or placeholder value.
'';
default = "unused_with_trust_auth";
example = "secure-password-here";
};
passwordSecretName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Name of the secret containing the PostgreSQL password.
The secret should be placed at /run/secrets/<n>.
If null, peer/socket authentication is assumed.
'';
default = null;
example = "beiwe-db-password";
};
sslmode = lib.mkOption {
type = lib.types.enum [ "disable" "allow" "prefer" "require" "verify-ca" "verify-full" ];
description = ''
PostgreSQL SSL mode. For local development without SSL certificates,
use "disable". For production with SSL, use "require" or "verify-full".
See: https://www.postgresql.org/docs/current/libpq-ssl.html
'';
default = "prefer";
example = "disable";
};
createLocally = lib.mkOption {
type = lib.types.bool;
description = ''
Whether to create the database user locally.
This requires PostgreSQL to be running locally with trust or peer authentication.
'';
default = false;
};
};
# ==========================================================================
# S3/MinIO Configuration
# ==========================================================================
s3 = {
bucket = lib.mkOption {
type = lib.types.str;
description = "S3 bucket name for data storage.";
default = "beiwe-data";
};
accessKeyId = lib.mkOption {
type = lib.types.str;
description = "AWS/MinIO access key ID.";
default = "";
example = "minioadmin";
};
secretAccessKey = lib.mkOption {
type = lib.types.str;
description = "AWS/MinIO secret access key.";
default = "";
example = "minioadmin";
};
endpoint = lib.mkOption {
type = lib.types.str;
description = ''
Custom S3 endpoint URL for MinIO or other S3-compatible storage.
Leave empty for AWS S3.
'';
default = "";
example = "http://localhost:9000";
};
};
# ==========================================================================
# Sentry Configuration (Optional)
# ==========================================================================
sentry = {
dsn = lib.mkOption {
type = lib.types.str;
description = "Sentry DSN for error tracking. Leave empty to disable.";
default = "";
example = "https://xxx@sentry.io/xxx";
};
};
# ==========================================================================
# Celery Configuration (Optional - for background tasks)
# ==========================================================================
celery = {
enable = lib.mkOption {
type = lib.types.bool;
description = ''
Enable Celery worker for background task processing.
When enabled, the following features become available:
- Push notifications to mobile apps
- Data processing pipelines
- Forest analysis integration
Requires RabbitMQ to be running and accessible.
'';
default = false;
};
rabbitmq = {
host = lib.mkOption {
type = lib.types.str;
description = "RabbitMQ host for Celery broker.";
default = "127.0.0.1";
example = "rabbitmq.example.com";
};
port = lib.mkOption {
type = lib.types.int;
description = "RabbitMQ port.";
default = 5672;
};
user = lib.mkOption {
type = lib.types.str;
description = "RabbitMQ user.";
default = "guest";
example = "beiwe";
};
password = lib.mkOption {
type = lib.types.str;
description = "RabbitMQ password.";
default = "guest";
example = "secure-password";
};
vhost = lib.mkOption {
type = lib.types.str;
description = "RabbitMQ virtual host.";
default = "";
example = "beiwe";
};
};
concurrency = lib.mkOption {
type = lib.types.int;
description = "Number of concurrent Celery worker processes.";
default = 2;
};
queues = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
Celery queues to process. Beiwe uses separate queues for different tasks:
- celery (default queue)
- data_processing
- push_notifications
- forest
'';
default = [ "celery" "data_processing" "push_notifications" "forest" ];
};
logLevel = lib.mkOption {
type = lib.types.enum [ "DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL" ];
description = "Celery worker log level.";
default = "INFO";
};
};
# ==========================================================================
# Gunicorn Configuration
# ==========================================================================
gunicorn = {
workers = lib.mkOption {
type = lib.types.int;
description = "Number of Gunicorn worker processes.";
default = 4;
};
threads = lib.mkOption {
type = lib.types.int;
description = "Number of threads per worker.";
default = 2;
};
timeout = lib.mkOption {
type = lib.types.int;
description = "Request timeout in seconds.";
default = 120;
};
};
# ==========================================================================
# Extra Environment Variables
# ==========================================================================
extraEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = ''
Additional environment variables for beiwe-backend.
These are passed directly to the service.
'';
default = {};
example = lib.literalExpression ''
{
DEBUG = "false";
}
'';
};
# ==========================================================================
# Reverse Proxy Configuration
# ==========================================================================
reverseProxy = {
enable = lib.mkOption {
type = lib.types.bool;
description = "Enable nginx reverse proxy for beiwe-backend.";
default = false;
};
hostName = lib.mkOption {
type = lib.types.str;
description = "Hostname for the reverse proxy.";
default = "localhost";
example = "beiwe.example.com";
};
ssl = lib.mkOption {
type = lib.types.bool;
description = "Enable SSL/HTTPS for the reverse proxy.";
default = false;
};
};
};
config = lib.mkIf cfg.enable {
# ==========================================================================
# Beiwe User and Group
# ==========================================================================
users.users.beiwe = {
isSystemUser = true;
group = "beiwe";
home = cfg.dataDir;
createHome = true;
description = "Beiwe backend service user";
};
users.groups.beiwe = {};
# ==========================================================================
# Beiwe Backend Systemd Service (Web Server)
# ==========================================================================
systemd.services.beiwe-backend = {
description = "Beiwe Backend - Digital Phenotyping Research Platform";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "postgresql.service" ] ++
lib.optionals cfg.reverseProxy.enable [ "nginx.service" ] ++
lib.optionals cfg.celery.enable [ "rabbitmq.service" ];
wants = lib.optionals cfg.database.createLocally [
"beiwe-db-setup.service"
];
requires = lib.optionals cfg.database.createLocally [
"postgresql.service"
];
environment = beiweEnvironment;
# Load database password from secret file if specified
serviceConfig = {
Type = "simple";
User = "beiwe";
Group = "beiwe";
WorkingDirectory = "${beiwePackage}/lib/beiwe-backend";
ExecStartPre = let
preStartScript = pkgs.writeShellScript "beiwe-pre-start" ''
# Run database migrations
${beiwePackage}/bin/beiwe-manage migrate --noinput || true
'';
in "+${preStartScript}";
ExecStart = ''
${beiwePackage}/bin/beiwe-gunicorn wsgi:application \
--bind ${cfg.bindToIp}:${toString cfg.bindToPort} \
--workers ${toString cfg.gunicorn.workers} \
--threads ${toString cfg.gunicorn.threads} \
--timeout ${toString cfg.gunicorn.timeout} \
--access-logfile - \
--error-logfile -
'';
Restart = "on-failure";
RestartSec = "5s";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir ];
} // (lib.optionalAttrs (cfg.database.passwordSecretName != null) {
EnvironmentFile = "/run/secrets/${cfg.database.passwordSecretName}";
});
};
# ==========================================================================
# Beiwe Celery Worker Service (Background Task Processing)
# ==========================================================================
#
# Uses jhsware fork which supports CELERY_MANAGER_IP and CELERY_PASSWORD
# environment variables instead of requiring a manager_ip file.
systemd.services.beiwe-celery-worker = lib.mkIf cfg.celery.enable {
description = "Beiwe Celery Worker - Background Task Processing";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "postgresql.service" "rabbitmq.service" ];
requires = [ "rabbitmq.service" ];
wants = [ "beiwe-backend.service" ];
environment = beiweEnvironment;
serviceConfig = {
Type = "simple";
User = "beiwe";
Group = "beiwe";
WorkingDirectory = "${beiwePackage}/lib/beiwe-backend";
# Celery command pattern from beiwe-backend wiki:
# python3 -m celery -A services.celery_data_processing worker -Q ...
ExecStart = let
queuesArg = lib.concatStringsSep "," cfg.celery.queues;
in ''
${beiwePackage}/bin/beiwe-celery \
-A services.celery_data_processing \
worker \
--queues=${queuesArg} \
--concurrency=${toString cfg.celery.concurrency} \
--loglevel=${cfg.celery.logLevel}
'';
Restart = "on-failure";
RestartSec = "10s";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir "/tmp" ];
} // (lib.optionalAttrs (cfg.database.passwordSecretName != null) {
EnvironmentFile = "/run/secrets/${cfg.database.passwordSecretName}";
});
};
# ==========================================================================
# Beiwe Celery Beat Service (Scheduled Tasks)
# ==========================================================================
systemd.services.beiwe-celery-beat = lib.mkIf cfg.celery.enable {
description = "Beiwe Celery Beat - Task Scheduler";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "rabbitmq.service" "beiwe-celery-worker.service" ];
requires = [ "rabbitmq.service" ];
wants = [ "beiwe-celery-worker.service" ];
environment = beiweEnvironment;
serviceConfig = {
Type = "simple";
User = "beiwe";
Group = "beiwe";
WorkingDirectory = "${beiwePackage}/lib/beiwe-backend";
ExecStart = ''
${beiwePackage}/bin/beiwe-celery \
-A services.celery_data_processing \
beat \
--loglevel=${cfg.celery.logLevel} \
--schedule=${cfg.dataDir}/celerybeat-schedule
'';
Restart = "on-failure";
RestartSec = "10s";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir ];
} // (lib.optionalAttrs (cfg.database.passwordSecretName != null) {
EnvironmentFile = "/run/secrets/${cfg.database.passwordSecretName}";
});
};
# ==========================================================================
# PostgreSQL Database Setup (Optional)
# ==========================================================================
systemd.services.beiwe-db-setup = lib.mkIf cfg.database.createLocally {
description = "Create Beiwe database and user";
wantedBy = [ "multi-user.target" ];
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
before = [ "beiwe-backend.service" ];
requiredBy = [ "beiwe-backend.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "postgres";
};
script = let
dbUser = cfg.database.user;
dbName = cfg.database.name;
dbHost = cfg.database.host;
dbPort = toString cfg.database.port;
in ''
set -euo pipefail
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL to be ready..."
until ${pkgs.postgresql}/bin/pg_isready -h ${dbHost} -p ${dbPort} 2>/dev/null; do
sleep 1
done
echo "PostgreSQL is ready"
# Create database user if it doesn't exist
echo "Checking if user '${dbUser}' exists..."
if ! ${pkgs.postgresql}/bin/psql -h ${dbHost} -p ${dbPort} -tAc "SELECT 1 FROM pg_roles WHERE rolname='${dbUser}'" | grep -q 1; then
echo "Creating user '${dbUser}'..."
${pkgs.postgresql}/bin/psql -h ${dbHost} -p ${dbPort} -c "CREATE USER ${dbUser}"
else
echo "User '${dbUser}' already exists"
fi
# Create database if it doesn't exist
echo "Checking if database '${dbName}' exists..."
if ! ${pkgs.postgresql}/bin/psql -h ${dbHost} -p ${dbPort} -tAc "SELECT 1 FROM pg_database WHERE datname='${dbName}'" | grep -q 1; then
echo "Creating database '${dbName}'..."
${pkgs.postgresql}/bin/psql -h ${dbHost} -p ${dbPort} -c "CREATE DATABASE ${dbName} OWNER ${dbUser}"
else
echo "Database '${dbName}' already exists"
fi
# Grant privileges on database (idempotent)
echo "Granting privileges..."
${pkgs.postgresql}/bin/psql -h ${dbHost} -p ${dbPort} -c "GRANT ALL PRIVILEGES ON DATABASE ${dbName} TO ${dbUser}" || true
${pkgs.postgresql}/bin/psql -h ${dbHost} -p ${dbPort} -d ${dbName} -c "GRANT ALL ON SCHEMA public TO ${dbUser}" || true
echo "Database setup complete"
'';
};
# ==========================================================================
# Nginx Reverse Proxy (Optional)
# ==========================================================================
services.nginx = lib.mkIf cfg.reverseProxy.enable {
enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = cfg.reverseProxy.ssl;
virtualHosts.${cfg.reverseProxy.hostName} = {
forceSSL = cfg.reverseProxy.ssl;
enableACME = cfg.reverseProxy.ssl;
locations."/" = {
proxyPass = "http://${cfg.bindToIp}:${toString cfg.bindToPort}";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout ${toString cfg.gunicorn.timeout}s;
proxy_connect_timeout ${toString cfg.gunicorn.timeout}s;
client_max_body_size 100M;
'';
};
# Static files
locations."/static/" = {
alias = "${beiwePackage}/lib/beiwe-backend/frontend/static/";
extraConfig = ''
expires 30d;
add_header Cache-Control "public, immutable";
'';
};
};
};
# ==========================================================================
# Firewall Configuration
# ==========================================================================
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall (
[ cfg.bindToPort ] ++
(lib.optionals cfg.reverseProxy.enable [ 80 443 ])
);
# ==========================================================================
# Utilities
# ==========================================================================
environment.systemPackages = [
beiwePackage
pkgs.curl
pkgs.jq
];
};
}
@@ -0,0 +1,309 @@
# Beiwe Backend package
# A Django-based smartphone digital phenotyping research platform backend
#
# Using jhsware fork which adds environment variable support for Celery configuration
# (CELERY_MANAGER_IP and CELERY_PASSWORD instead of manager_ip file)
#
# To update to a new version:
# 1. Update the rev to the new commit hash
# 2. Run: nix-prefetch-url --unpack https://github.com/jhsware/beiwe-backend/archive/<NEW_COMMIT>.tar.gz
# 3. Update the hash with the output from step 2
{
lib,
stdenv,
fetchFromGitHub,
fetchPypi,
python312,
python312Packages,
postgresql,
# Custom parameters
rev ? "93be878", # jhsware fork with env var support for Celery
}:
let
# Known version hashes
# To add a new version, run:
# nix-prefetch-url --unpack https://github.com/jhsware/beiwe-backend/archive/<COMMIT>.tar.gz
versionHashes = {
# jhsware fork with CELERY_MANAGER_IP and CELERY_PASSWORD env var support
"93be878" = {
srcHash = "sha256-marYxVINxgW0X9x+xoHL7bdRYEgm+4q+O1CF5WXeZGg=";
};
# Original onnela-lab version (for reference)
"6bb5363" = {
srcHash = "sha256-EeD+I3mWC81mhmlO9cKzRrArDLKVBmqhZjgJI8+geu0=";
};
};
hashes = versionHashes.${rev} or (throw ''
beiwe-backend revision ${rev} is not supported.
Supported revisions: ${builtins.concatStringsSep ", " (builtins.attrNames versionHashes)}
To add support for revision ${rev}:
1. Get source hash: nix-prefetch-url --unpack https://github.com/jhsware/beiwe-backend/archive/${rev}.tar.gz
2. Add entry to versionHashes in app_modules/_unstable/beiwe-backend/package.nix
'');
# Build cronutils from PyPI (not available in nixpkgs)
cronutils = python312Packages.buildPythonPackage rec {
pname = "cronutils";
version = "0.4.2";
format = "setuptools";
src = fetchPypi {
inherit pname version;
hash = "sha256-SFHkQ9NltAyWArArkFpSBIJF3gMoXbxHEXreM1SEPUY=";
};
propagatedBuildInputs = with python312Packages; [
sentry-sdk
];
# Tests require network access
doCheck = false;
pythonImportsCheck = [ "cronutils" ];
meta = with lib; {
description = "Utilities for cron jobs including error handling";
homepage = "https://pypi.org/project/cronutils/";
license = licenses.mit;
};
};
# Build beiwe-forest from GitHub (Forest analysis library)
# See: https://github.com/onnela-lab/forest
# Note: pip install git+https://github.com/onnela-lab/forest
beiweForest = python312Packages.buildPythonPackage rec {
pname = "forest";
version = "unstable-2024-12-01";
format = "pyproject";
src = fetchFromGitHub {
owner = "onnela-lab";
repo = "forest";
rev = "develop"; # Main development branch
hash = "sha256-t+oq/jfJUmWCs0XzrN+xciYc3lz4oPiO8V8qfj3iTJA=";
};
nativeBuildInputs = with python312Packages; [
setuptools
];
propagatedBuildInputs = with python312Packages; [
# Core data science
numpy
pandas
scipy
scikit-learn
# Time/date utilities
pytz
holidays
timezonefinder
# GIS/mapping
shapely
pyproj
# Audio processing (for voice analysis)
librosa
# HTTP/API
requests
ratelimit
];
# Some optional dependencies not in nixpkgs (openrouteservice, ssqueezepy)
# Disable strict runtime deps check to allow partial functionality
pythonRelaxDeps = true;
pythonRemoveDeps = [ "openrouteservice" "ssqueezepy" ];
# Tests require data files
doCheck = false;
pythonImportsCheck = [ "forest" ];
meta = with lib; {
description = "Forest library for analyzing Beiwe digital phenotyping data";
homepage = "https://github.com/onnela-lab/forest";
license = licenses.bsd3;
};
};
# Python environment with all dependencies from requirements.txt
pythonEnv = python312.withPackages (ps: with ps; [
# Django and web framework
django
django-extensions
django-timezone-field # Provides timezone_field module
gunicorn
jinja2
# Database - using psycopg (v3) as specified in requirements.txt
psycopg
# AWS/S3 support
boto3
# Celery for task queue
celery
# Error tracking and monitoring
sentry-sdk
cronutils # Custom package built above
# Firebase (push notifications)
firebase-admin
# Security and crypto
pycryptodomex # Note: pycryptodomex not pycryptodome
pyotp
bleach
# Serialization
orjson
# Date/time utilities
python-dateutil
pytz
# Compression - pyzstd provides "import pyzstd" (jhsware fork uses pyzstd)
pyzstd
# Data analysis
numpy
pandas
scipy
scikit-learn
beiweForest # Custom package - provides "import forest"
# Other utilities
requests
rcssmin
pypng
pyqrcode
# Development/debugging
ipython
mypy
]);
in
stdenv.mkDerivation {
pname = "beiwe-backend";
version = rev;
src = fetchFromGitHub {
owner = "jhsware"; # Fork with env var support for Celery
repo = "beiwe-backend";
inherit rev;
hash = hashes.srcHash;
};
buildInputs = [
pythonEnv
postgresql
];
# No build phase needed - this is a Python application
dontBuild = true;
# Patch Django settings to support DATABASE_SSLMODE environment variable
# This allows controlling PostgreSQL SSL mode via environment variable
postPatch = ''
# Find the Django settings file and patch the database configuration
# to include sslmode from environment variable
# Add sslmode support to database configuration
# This sed command finds the DATABASES dict and adds OPTIONS with sslmode
if [ -f config/django_settings.py ]; then
echo "Patching config/django_settings.py for DATABASE_SSLMODE support..."
# Add import for os at the top if not already there
if ! grep -q "^import os" config/django_settings.py; then
sed -i '1i import os' config/django_settings.py
fi
# Append code to add sslmode to database options at the end of the file
cat >> config/django_settings.py << 'SSLPATCH'
# Patched by nix-infra-machine: Add DATABASE_SSLMODE support
# This allows setting PostgreSQL sslmode via environment variable
_db_sslmode = os.environ.get('DATABASE_SSLMODE', os.environ.get('PGSSLMODE', 'prefer'))
if 'default' in DATABASES:
if 'OPTIONS' not in DATABASES['default']:
DATABASES['default']['OPTIONS'] = {}
DATABASES['default']['OPTIONS']['sslmode'] = _db_sslmode
SSLPATCH
echo "Patched database settings for sslmode support"
else
echo "Warning: config/django_settings.py not found, skipping sslmode patch"
fi
'';
installPhase = ''
runHook preInstall
# Create directory structure
mkdir -p $out/lib/beiwe-backend
mkdir -p $out/bin
# Copy all source files
cp -r . $out/lib/beiwe-backend/
# Create wrapper scripts
cat > $out/bin/beiwe-manage <<EOF
#!/usr/bin/env bash
cd $out/lib/beiwe-backend
exec ${pythonEnv}/bin/python manage.py "\$@"
EOF
chmod +x $out/bin/beiwe-manage
cat > $out/bin/beiwe-gunicorn <<EOF
#!/usr/bin/env bash
cd $out/lib/beiwe-backend
exec ${pythonEnv}/bin/gunicorn "\$@"
EOF
chmod +x $out/bin/beiwe-gunicorn
cat > $out/bin/beiwe-celery <<EOF
#!/usr/bin/env bash
cd $out/lib/beiwe-backend
exec ${pythonEnv}/bin/celery "\$@"
EOF
chmod +x $out/bin/beiwe-celery
runHook postInstall
'';
meta = {
description = "Beiwe - smartphone-based digital phenotyping research platform backend";
longDescription = ''
The Beiwe Research Platform collects high-throughput smartphone-based
digital phenotyping data including spatial trajectories (GPS), physical
activity patterns (accelerometer/gyroscope), social networks and
communication dynamics (call/text logs), and voice samples.
This package provides the Django-based backend server that supports:
- Web-based study management portal
- API endpoints for iOS/Android mobile apps
- Data processing pipelines
This is the jhsware fork which adds environment variable support for
Celery configuration (CELERY_MANAGER_IP and CELERY_PASSWORD).
Patched by nix-infra-machine to support DATABASE_SSLMODE environment
variable for controlling PostgreSQL SSL mode.
'';
homepage = "https://github.com/jhsware/beiwe-backend";
license = lib.licenses.bsd3;
platforms = lib.platforms.unix;
};
}