Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added paperclip/data/.gitkeep
Empty file.
31 changes: 31 additions & 0 deletions paperclip/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: "3.7"

services:
app_proxy:
environment:
APP_HOST: paperclip_server_1
APP_PORT: 3100
PROXY_AUTH_ADD: "false"

server:
image: ghcr.io/paperclipai/paperclip:sha-a072377@sha256:adfd3795fb98c2599c9b14443a49df86b8f08fb98f5ab7810e777d04c1ff6709
user: "1000:1000"
restart: on-failure
entrypoint:
- node
command:
- --import
- ./server/node_modules/tsx/dist/loader.mjs
- server/dist/index.js
environment:
HOST: "0.0.0.0"
PAPERCLIP_HOME: "/paperclip"
PAPERCLIP_PUBLIC_URL: "http://${DEVICE_DOMAIN_NAME}:8860"
PAPERCLIP_ALLOWED_HOSTNAMES: "${DEVICE_DOMAIN_NAME},${DEVICE_DOMAIN_NAME}:8860,${DEVICE_HOSTNAME},${DEVICE_HOSTNAME}:8860,${APP_DOMAIN},${APP_DOMAIN}:8860,${APP_HIDDEN_SERVICE},${APP_PAPERCLIP_LOCAL_IPS},paperclip_server_1"
BETTER_AUTH_SECRET: "${APP_SEED}"
PAPERCLIP_DEPLOYMENT_MODE: "authenticated"
PAPERCLIP_DEPLOYMENT_EXPOSURE: "private"
PAPERCLIP_TELEMETRY_DISABLED: "1"
DO_NOT_TRACK: "1"
volumes:
- ${APP_DATA_DIR}/data:/paperclip
2 changes: 2 additions & 0 deletions paperclip/exports.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
local_ips=$(hostname --all-ip-addresses 2> /dev/null | tr ' ' ',') || local_ips=""
export APP_PAPERCLIP_LOCAL_IPS="${local_ips}"
56 changes: 56 additions & 0 deletions paperclip/hooks/post-start
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail

APP_CONTAINER="paperclip_server_1"
HEALTH_URL="http://127.0.0.1:3100/api/health"
CONFIG_FILE="/paperclip/instances/default/config.json"
PUBLIC_URL="http://${DEVICE_DOMAIN_NAME}:8860"
INVITE_FILE="${APP_DATA_DIR}/data/bootstrap-invite-url.txt"

for _ in $(seq 1 60); do
if health_json=$(docker exec "${APP_CONTAINER}" curl -fsS "${HEALTH_URL}" 2>/dev/null); then
break
fi
sleep 2
done

if [ -z "${health_json:-}" ]; then
echo "Paperclip post-start: server never became healthy"
exit 0
fi

bootstrap_status=$(python3 - <<'PY' "$health_json"
import json, sys
obj = json.loads(sys.argv[1])
print(obj.get("bootstrapStatus", ""))
PY
)

bootstrap_invite_active=$(python3 - <<'PY' "$health_json"
import json, sys
obj = json.loads(sys.argv[1])
print("true" if obj.get("bootstrapInviteActive") else "false")
PY
)

if [ "${bootstrap_status}" != "bootstrap_pending" ]; then
echo "Paperclip post-start: bootstrap not required"
exit 0
fi

if [ "${bootstrap_invite_active}" = "true" ]; then
echo "Paperclip post-start: bootstrap invite already active"
exit 0
fi

invite_output=$(docker exec "${APP_CONTAINER}" npx --yes paperclipai auth bootstrap-ceo --config "${CONFIG_FILE}" --base-url "${PUBLIC_URL}" 2>&1 || true)
invite_url=$(printf '%s\n' "${invite_output}" | sed -n 's/.*Invite URL: //p' | tail -1)

if [ -n "${invite_url}" ]; then
printf '%s\n' "${invite_url}" > "${INVITE_FILE}"
chown 1000:1000 "${INVITE_FILE}" || true
echo "Paperclip bootstrap invite: ${invite_url}"
else
echo "Paperclip post-start: failed to generate bootstrap invite"
printf '%s\n' "${invite_output}"
fi
24 changes: 24 additions & 0 deletions paperclip/hooks/pre-start
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="${APP_DATA_DIR}/data/instances/default"
CONFIG_FILE="${CONFIG_DIR}/config.json"
PUBLIC_URL="http://${DEVICE_DOMAIN_NAME}:8860"

mkdir -p "${CONFIG_DIR}"

if [ ! -f "${CONFIG_FILE}" ]; then
cat > "${CONFIG_FILE}" <<EOF
{
"\$meta": { "version": 1, "updatedAt": "2026-04-06T00:00:00.000Z", "source": "onboard" },
"database": { "mode": "embedded-postgres" },
"logging": { "mode": "file" },
"server": { "deploymentMode": "authenticated", "exposure": "private", "host": "0.0.0.0", "port": 3100 },
"auth": { "baseUrlMode": "explicit", "publicBaseUrl": "${PUBLIC_URL}" },
"storage": { "provider": "local_disk" },
"secrets": { "provider": "local_encrypted", "strictMode": false }
}
EOF
fi

chown -R 1000:1000 "${APP_DATA_DIR}/data"
30 changes: 30 additions & 0 deletions paperclip/umbrel-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
manifestVersion: 1
id: paperclip
name: Paperclip
tagline: Open-source orchestration for autonomous AI companies
category: ai
version: "2026.403.0"
port: 8860
description: >-
Paperclip lets you run AI agents with roles, budgets, tools, and a shared workspace from one web UI.
You can organize work into companies, teams, tickets, and goals, then track what agents did with logs,
approvals, and audit history.

This Umbrel package uses Paperclip's embedded database, so all data stays inside the app's data directory.
On first launch, Paperclip creates a bootstrap invite for the first admin account and saves the invite URL in the app logs.

To do real work, you'll still need to connect your own model providers and agent tools, such as OpenAI,
Anthropic, Codex, Claude Code, or other integrations supported by Paperclip.
developer: PaperclipAI
website: https://paperclip.ing/
submitter: aidencole98
submission: https://github.com/getumbrel/umbrel-apps/pull/5298
repo: https://github.com/paperclipai/paperclip
support: https://github.com/paperclipai/paperclip/issues
gallery: []
releaseNotes: ""
dependencies: []
path: ""
defaultUsername: ""
defaultPassword: ""
deterministicPassword: false