Skip to content
This repository was archived by the owner on Jul 26, 2026. It is now read-only.
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to Upstand are recorded here. Release tags use semantic vers

## Unreleased

## 0.1.151 - 2026-07-26

### Fixed & Enhanced

- Propagated configured CA certificates through backup create, restore, verify, and deletion runtimes for S3-compatible destinations.
- Prevented deletion of certificates that are still assigned to S3 backup destinations.
- Centralized backup destination certificate enrichment and standardized the local development encryption-key example.

## 0.1.150 - 2026-07-26

### Added & Enhanced
Expand Down
10 changes: 9 additions & 1 deletion apps/fumadocs/content/docs/features/upstand-json.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,20 @@ Define container resource limits, entrypoint commands, replica scaling, and rest

### C. Scheduled Cron Jobs (`crons`)

HTTP cron authentication is intentionally indirect: set the named variable in
the resource's encrypted Environment configuration, then reference only its
name with `secretEnvVar`. Upstand never accepts or stores a bearer-token value
from `upstand.json`; a missing or empty referenced variable causes the run to
fail instead of sending an unauthenticated request.

Declare recurring HTTP route cron jobs or in-container script schedules.

| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `schedule` | `string` | **(Required)** | Cron expression schedule (e.g. `0 0 * * *` for daily at midnight). |
| `path` | `string` | — | HTTP route endpoint path for HTTP cron jobs (must start with `/`). |
| `method` | `string` | `"GET"` | HTTP method (`GET` or `POST`). |
| `secret` | `string` | — | Authorization token sent as `Authorization: Bearer <secret>`. |
| `secretEnvVar` | `string` | — | Name of the encrypted resource environment variable containing the bearer token. Never put the secret value in `upstand.json`. |
| `command` | `string` | — | In-container script command to execute. |
| `shellType` | `string` | `"bash"` | Shell type (`"bash"` or `"sh"`). |
| `timezone` | `string` | `"UTC"` | IANA timezone identifier (e.g. `"UTC"`, `"America/New_York"`). |
Expand All @@ -120,6 +126,8 @@ Declare recurring HTTP route cron jobs or in-container script schedules.
{
"name": "Daily Cleanup",
"path": "/api/cron/cleanup",
"method": "POST",
"secretEnvVar": "CRON_SECRET",
"schedule": "0 0 * * *"
},
{
Expand Down
9 changes: 7 additions & 2 deletions apps/fumadocs/public/upstand.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,25 @@
},
"buildPath": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"default": ".",
"description": "Relative directory path where build commands should execute."
},
"dockerfilePath": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"default": "Dockerfile",
"description": "Relative path to the Dockerfile."
},
"dockerContextPath": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"default": ".",
"description": "Relative path to the Docker build context."
},
"publishDirectory": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"description": "Relative directory where static artifacts are published."
},
"dockerBuildStage": {
Expand Down Expand Up @@ -109,9 +113,10 @@
"default": "GET",
"description": "HTTP method for HTTP cron requests."
},
"secret": {
"secretEnvVar": {
"type": "string",
"description": "Authorization secret token for HTTP cron requests."
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
"description": "Name of the encrypted resource environment variable containing the HTTP Authorization bearer token."
},
"command": {
"type": "string",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ CORS_ORIGIN=http://localhost:3001
REDIS_URL=redis://localhost:6379
NODE_ENV=development
IS_CLOUD=false
ENCRYPTION_KEY_V1=dev-encryption-key-32-chars-long
ENCRYPTION_KEY_V1=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=
UPSTAND_INSTANCE_OWNER_USER_ID=
9 changes: 7 additions & 2 deletions apps/web/public/upstand.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,25 @@
},
"buildPath": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"default": ".",
"description": "Relative directory path where build commands should execute."
},
"dockerfilePath": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"default": "Dockerfile",
"description": "Relative path to the Dockerfile."
},
"dockerContextPath": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"default": ".",
"description": "Relative path to the Docker build context."
},
"publishDirectory": {
"type": "string",
"pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+",
"description": "Relative directory where static artifacts are published."
},
"dockerBuildStage": {
Expand Down Expand Up @@ -109,9 +113,10 @@
"default": "GET",
"description": "HTTP method for HTTP cron requests."
},
"secret": {
"secretEnvVar": {
"type": "string",
"description": "Authorization secret token for HTTP cron requests."
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
"description": "Name of the encrypted resource environment variable containing the HTTP Authorization bearer token."
},
"command": {
"type": "string",
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ services:
CORS_ORIGIN: http://localhost:3001
REDIS_URL: redis://redis:6379
DOCKER_NETWORK: upstand-network
ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1:-dev-encryption-key-32-chars-long}
ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1:-MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -86,7 +86,7 @@ services:
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-upstand-local-development-secret-that-is-at-least-32-characters}
REDIS_URL: redis://redis:6379
SCHEDULES_PORT: 3002
ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1:-dev-encryption-key-32-chars-long}
ENCRYPTION_KEY_V1: ${ENCRYPTION_KEY_V1:-MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
depends_on:
postgres:
condition: service_healthy
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "upstand",
"version": "0.1.150",
"version": "0.1.151",
"private": true,
"workspaces": {
"packages": [
Expand Down
11 changes: 9 additions & 2 deletions packages/api/src/di/backups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,14 @@ export function registerBackups(
const envs = parseResourceEnvironmentVariables(
resourceSecret.envVars,
);
cronSecret = envs.CRON_SECRET || "";
cronSecret = schedule.secretEnvVar
? envs[schedule.secretEnvVar] || ""
: envs.CRON_SECRET || "";
}
Comment on lines +187 to +190

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Read cron secrets from the hydrated resource

When an HTTP cron specifies secretEnvVar, it always fails with the new “missing or empty” error in production: DrizzleUnitOfWork does not expose a resourceSecretRepository, so the optional lookup inside this transaction always returns undefined, regardless of whether the variable is configured. The resource loaded above is already hydrated with encrypted envVars; parse that field directly or add a real repository to the unit of work before relying on this value.

Useful? React with 👍 / 👎.

if (schedule.secretEnvVar && !cronSecret) {
throw new Error(
`Configured cron secret environment variable '${schedule.secretEnvVar}' is missing or empty`,
);
}

const path = schedule.command.startsWith("/")
Expand All @@ -203,7 +210,7 @@ export function registerBackups(
const timeoutId = setTimeout(() => controller.abort(), 30_000);

const response = await fetch(targetUrl, {
method: "GET",
method: schedule.httpMethod || "GET",
headers,
signal: controller.signal,
});
Expand Down
11 changes: 9 additions & 2 deletions packages/api/src/routers/certificate.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,23 @@ export const certificateRouter = router({
return false;
}
});
const s3Destinations =
await uow.s3DestinationRepository.findByOrganizationId(
existing.organizationId,
);
const isS3DestinationInUse = s3Destinations.some(
(destination) => destination.certificateId === existing.id,
);
const settings = await uow.webServerSettingsRepository.findGlobal();
const isServerDomainInUse =
settings?.certificateProvider === "custom" &&
settings?.certificateId === existing.id;

if (isInUse || isServerDomainInUse) {
if (isInUse || isServerDomainInUse || isS3DestinationInUse) {
throw new TRPCError({
code: "CONFLICT",
message:
"This certificate is currently assigned to a resource domain or server domain. Remove that assignment before deleting it.",
"This certificate is currently assigned to a resource domain, server domain, or S3 backup destination. Remove that assignment before deleting it.",
});
}
const result = await ctx.scope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "schedule" ADD COLUMN "http_method" text;--> statement-breakpoint
ALTER TABLE "schedule" ADD COLUMN "secret_env_var" text;
Loading
Loading