Skip to content

feat: Multi-tenant SaaS Support with TradingView Webhook Integration#3

Open
gn00295120 wants to merge 7 commits intoluisleo526:mainfrom
gn00295120:feature/saas-support
Open

feat: Multi-tenant SaaS Support with TradingView Webhook Integration#3
gn00295120 wants to merge 7 commits intoluisleo526:mainfrom
gn00295120:feature/saas-support

Conversation

@gn00295120
Copy link

@gn00295120 gn00295120 commented Jan 2, 2026

Summary

  • Multi-tenant SaaS architecture: Gateway + Admin API + per-tenant Docker workers
  • TradingView webhook integration: Receive trading signals via /webhook/{tenant_slug}
  • Credential management: Encrypted storage for Shioaji API keys with master key
  • Synology NAS deployment: Complete Docker Compose setup with Cloudflare Tunnel
  • Security hardening: Environment variables for all secrets, log sanitization

Key Features

Multi-tenant Architecture

  • Gateway routes requests to tenant-specific workers
  • Admin API manages tenants, credentials, and worker lifecycle
  • Workers are spawned/stopped dynamically per tenant

TradingView Webhook

  • POST /webhook/{tenant_slug} endpoint for trading signals
  • Auto-starts worker on first webhook if credentials exist
  • Secure authentication via tenant-specific tokens

Deployment Options

  • Standard Docker Compose (docker-compose.yaml)
  • Multi-tenant mode (docker-compose.multitenant.yaml)
  • Synology NAS with Cloudflare Tunnel (deploy/synology/)

Files Changed

Category Files
Gateway gateway/main.py
Admin API admin/main.py, admin/tenant_service.py
Worker Manager orchestrator/worker_manager.py
Credentials credentials/manager.py, credentials/encryption.py
Models models/tenant.py
Security utils/log_sanitizer.py
Deployment deploy/synology/*, docker-compose.multitenant.yaml

Test plan

  • E2E tests pass for credential status API
  • Worker lifecycle (start/stop) verified
  • Webhook endpoint tested with mock TradingView signals
  • Production deployment on Synology NAS

🤖 Generated with Claude Code


Note

Enables a managed multi-tenant trading backend with tenant isolation, secure credentials, and webhook-driven trading.

  • Introduces Admin API (admin/main.py) to manage tenants, credentials (encrypted), webhooks, and worker lifecycle with rate limiting and CORS
  • Adds Gateway (gateway/main.py) with tenant-scoped routes (/api/v1/{tenant_slug}/...) and a secure TradingView POST /webhook that logs/validates requests and enqueues trades
  • Implements Worker Manager (orchestrator/worker_manager.py) to provision per-tenant Docker containers, inject secrets, allocate Redis DBs, and start/stop/hibernate workers
  • Adds multi-tenant models and migrations: tenants, tenant_credentials, worker_instances, tenant_audit_log, webhook_logs; links order_history to tenant_id
  • Updates core services for multitenancy: trading_queue.py (tenant-prefixed queues), trading_worker.py (secrets via files, CA support, mock mode), main.py (Bearer auth/CORS, new /api/v1/* endpoints), and sanitized logging (utils/log_sanitizer.py)
  • Provides deployment configs: docker-compose.multitenant.yaml, Synology setup (deploy/synology/*), Dockerfile.worker; expands .gitignore and requirements.txt

Written by Cursor Bugbot for commit df3c96a. This will update automatically on new commits. Configure here.

gn00295120 and others added 7 commits January 1, 2026 14:29
- Add BACKEND_API_TOKEN env var for SaaS frontend connection
- Add ALLOWED_ORIGINS env var for configurable CORS
- Add verify_auth() supporting both Bearer token and X-Auth-Key
- Add /api/v1/ping endpoint for connection testing
- Add /api/v1/status endpoint for system health check
- Add /api/v1/info endpoint for backend information
- Add /api/v1/orders and /api/v1/positions with new auth
- Maintain backward compatibility with existing auth

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update health check endpoint response format
- Improve trading worker stability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Security fixes:
- Use secrets.compare_digest() for token comparison (timing attack prevention)
- Add startup warning when running with default credentials
- Fix DB session crash when SessionLocal() fails

Code quality fixes:
- Use datetime with UTC timezone for API responses
- Use specific exception types (ConnectionError, TimeoutError, SQLAlchemyError)
- Strip whitespace from ALLOWED_ORIGINS and SUPPORTED_FUTURES parsing

Addresses feedback from Gemini, Copilot, and Codex reviews.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Multi-tenant infrastructure:
- Admin API for tenant CRUD with owner_id isolation
- Secure slug generation with random prefix
- Worker orchestration via Docker containers
- Per-tenant Redis DB isolation (0-15)

TradingView webhook support:
- Enable/disable webhook per tenant
- Auto-generated webhook secret
- Request validation and logging
- Support for TradingView alert actions

Credential management:
- Shioaji API key upload/storage
- CA certificate support
- ready_for_trading status flags
- Frontend-compatible credential status API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- docker-compose.yml optimized for Synology
- Cloudflare Tunnel integration (免開 Port)
- Auto-setup script with security key generation
- Environment template with all required variables
- Comprehensive README with deployment guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace hardcoded 'postgres' password with environment variable
${POSTGRES_PASSWORD:-postgres} to resolve GitGuardian security alert.

- docker-compose.yaml: use env var for db-migrate and db services
- docker-compose.multitenant.yaml: use env var for all database URLs
- example.env: add POSTGRES_PASSWORD variable with default value

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 2, 2026 07:15
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a comprehensive multi-tenant SaaS architecture for the Shioaji trading backend, enabling multiple isolated tenants with TradingView webhook integration. The implementation includes gateway-based routing, admin API for tenant management, per-tenant Docker workers, encrypted credential storage, and Synology NAS deployment support with Cloudflare Tunnel.

Key Changes:

  • Multi-tenant architecture with gateway routing to isolated tenant workers
  • TradingView webhook endpoint with action parsing and secret validation
  • Encrypted credential management with Docker secrets support

Reviewed changes

Copilot reviewed 27 out of 29 changed files in this pull request and generated 31 comments.

Show a summary per file
File Description
gateway/main.py API gateway routing requests to tenant-specific workers with webhook support
admin/main.py Admin API for tenant, credential, and worker lifecycle management
admin/tenant_service.py Tenant CRUD service with secure slug generation and credential handling
orchestrator/worker_manager.py Docker-based worker manager for per-tenant container lifecycle
models/tenant.py Multi-tenant data models with audit logging
trading_worker.py Enhanced worker with multi-tenant support and Docker secrets
trading_queue.py Queue client with tenant-based queue prefixing
utils/log_sanitizer.py Log sanitization to prevent credential leakage
docker-compose.multitenant.yaml Multi-tenant deployment configuration
deploy/synology/* Synology NAS deployment scripts and configuration
db/migrations/002-004*.sql Database migrations for tenant management and webhooks
example.env Updated environment configuration with SaaS settings

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +67
# 如果兩種都沒設定,允許通過(向下相容舊版無認證設定)
# 注意:這是不安全的,啟動時會有警告
if not BACKEND_API_TOKEN and AUTH_KEY == "changeme":
return True
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

The AUTH_KEY default value "changeme" combined with the security logic creates an insecure default configuration. When both AUTH_KEY is "changeme" and BACKEND_API_TOKEN is empty, the verify_auth function allows all requests without authentication (lines 64-67). While there's a warning on startup, this is a dangerous default that could lead to production deployments with no authentication. Consider requiring at least one authentication method to be set, or failing to start the service if neither is configured properly.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +45
-- Add webhook_secret to tenants table (may already exist)
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS webhook_secret VARCHAR(64);
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS webhook_enabled BOOLEAN DEFAULT false;

-- Create webhook_logs table
CREATE TABLE IF NOT EXISTS webhook_logs (
id SERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,

-- Request info
source_ip VARCHAR(45),
request_body JSONB,
headers JSONB,

-- Processing result
status VARCHAR(20) NOT NULL DEFAULT 'received', -- received, validated, processed, failed
error_message TEXT,

-- Parsed TradingView data
tv_alert_name VARCHAR(255),
tv_ticker VARCHAR(50),
tv_action VARCHAR(20), -- buy, sell, long, short, exit, etc.
tv_quantity INTEGER,
tv_price DECIMAL(18, 4),

-- Order result
order_id INTEGER REFERENCES order_history(id),

-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP WITH TIME ZONE
);

-- Index for fast lookups
CREATE INDEX IF NOT EXISTS idx_webhook_logs_tenant_id ON webhook_logs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_webhook_logs_created_at ON webhook_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_webhook_logs_status ON webhook_logs(status);

-- Comment
COMMENT ON TABLE webhook_logs IS 'TradingView webhook request logs for audit and debugging';
COMMENT ON COLUMN tenants.webhook_secret IS 'Secret token for validating TradingView webhook requests';
COMMENT ON COLUMN tenants.webhook_enabled IS 'Whether webhook is enabled for this tenant';
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

The migration does not check if the webhook_logs table or its indexes already exist before attempting to create them. While using IF NOT EXISTS for the table is good, if the migration is run multiple times or if the table schema changes, there's no version tracking or migration history. Consider using a proper migration framework (like Alembic) or at least adding a migration tracking table to prevent issues with repeated executions.

Suggested change
-- Add webhook_secret to tenants table (may already exist)
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS webhook_secret VARCHAR(64);
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS webhook_enabled BOOLEAN DEFAULT false;
-- Create webhook_logs table
CREATE TABLE IF NOT EXISTS webhook_logs (
id SERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
-- Request info
source_ip VARCHAR(45),
request_body JSONB,
headers JSONB,
-- Processing result
status VARCHAR(20) NOT NULL DEFAULT 'received', -- received, validated, processed, failed
error_message TEXT,
-- Parsed TradingView data
tv_alert_name VARCHAR(255),
tv_ticker VARCHAR(50),
tv_action VARCHAR(20), -- buy, sell, long, short, exit, etc.
tv_quantity INTEGER,
tv_price DECIMAL(18, 4),
-- Order result
order_id INTEGER REFERENCES order_history(id),
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP WITH TIME ZONE
);
-- Index for fast lookups
CREATE INDEX IF NOT EXISTS idx_webhook_logs_tenant_id ON webhook_logs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_webhook_logs_created_at ON webhook_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_webhook_logs_status ON webhook_logs(status);
-- Comment
COMMENT ON TABLE webhook_logs IS 'TradingView webhook request logs for audit and debugging';
COMMENT ON COLUMN tenants.webhook_secret IS 'Secret token for validating TradingView webhook requests';
COMMENT ON COLUMN tenants.webhook_enabled IS 'Whether webhook is enabled for this tenant';
-- Migration tracking table to ensure this migration runs only once
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
DO $$
BEGIN
-- Only apply this migration if it has not been recorded yet
IF NOT EXISTS (
SELECT 1 FROM schema_migrations WHERE id = '004_webhook_support'
) THEN
-- Add webhook_secret to tenants table (may already exist)
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS webhook_secret VARCHAR(64);
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS webhook_enabled BOOLEAN DEFAULT false;
-- Create webhook_logs table
CREATE TABLE IF NOT EXISTS webhook_logs (
id SERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
-- Request info
source_ip VARCHAR(45),
request_body JSONB,
headers JSONB,
-- Processing result
status VARCHAR(20) NOT NULL DEFAULT 'received', -- received, validated, processed, failed
error_message TEXT,
-- Parsed TradingView data
tv_alert_name VARCHAR(255),
tv_ticker VARCHAR(50),
tv_action VARCHAR(20), -- buy, sell, long, short, exit, etc.
tv_quantity INTEGER,
tv_price DECIMAL(18, 4),
-- Order result
order_id INTEGER REFERENCES order_history(id),
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP WITH TIME ZONE
);
-- Index for fast lookups
CREATE INDEX IF NOT EXISTS idx_webhook_logs_tenant_id ON webhook_logs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_webhook_logs_created_at ON webhook_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_webhook_logs_status ON webhook_logs(status);
-- Comment
COMMENT ON TABLE webhook_logs IS 'TradingView webhook request logs for audit and debugging';
COMMENT ON COLUMN tenants.webhook_secret IS 'Secret token for validating TradingView webhook requests';
COMMENT ON COLUMN tenants.webhook_enabled IS 'Whether webhook is enabled for this tenant';
-- Record that this migration has been applied
INSERT INTO schema_migrations (id) VALUES ('004_webhook_support');
END IF;
END
$$;

Copilot uses AI. Check for mistakes.
Comment on lines +673 to +694
def allocate_redis_db(self) -> int:
"""
Allocate an available Redis database number (0-15).

Returns the lowest available number, or raises an error if all are used.
"""
used_dbs = set(
row[0] for row in self.db.query(WorkerInstance.redis_db).filter(
WorkerInstance.status.in_([
WorkerStatus.PENDING.value,
WorkerStatus.STARTING.value,
WorkerStatus.RUNNING.value,
WorkerStatus.HIBERNATING.value,
])
).all()
)

for db_num in range(16):
if db_num not in used_dbs:
return db_num

raise TenantServiceError("No available Redis database slots (max 15 tenants)")
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

The allocate_redis_db method has a limit of 15 tenants (Redis databases 0-15), but the comment says "max 15 tenants" which is technically 16 databases (0-15 inclusive). The error message should clarify this is 16 total tenants. Additionally, this hard limit might be too restrictive for a SaaS architecture - consider documenting this limitation prominently and potentially implementing Redis clustering or using key prefixes instead of separate databases for better scalability.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +108
async def get_current_user_id(
x_user_id: Optional[str] = Header(None, alias="X-User-ID"),
) -> str:
"""
Extract user ID from X-User-ID header.

In production, this should be validated against the auth token.
The frontend should extract the user ID from the Supabase session
and pass it in this header.
"""
if not x_user_id:
raise HTTPException(
status_code=401,
detail="Missing X-User-ID header. User authentication required."
)
return x_user_id
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

The user_id from X-User-ID header is used directly for ownership checks without any validation against the authentication token. This means any authenticated user can impersonate another user by simply changing the X-User-ID header. The comment on lines 99-101 acknowledges this issue ("In production, this should be validated against the auth token"), but this is a critical security vulnerability. The user_id should be extracted from the verified JWT token, not from an unvalidated header.

Copilot uses AI. Check for mistakes.
response = client.place_entry_order(
symbol=symbol,
quantity=quantity,
action="Buy" if internal_action == "long_entry" else "Sell",
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

The action mapping for "Sell" in entry orders should be "Sell" for short entry, but the action parameter passed to place_entry_order should match the TradingView action format. The current code passes "Buy" for long_entry and "Sell" for short_entry, but based on the action parameter in the OrderRequest model and trading logic, this should use the internal action format (e.g., "long_entry" or "short_entry") or the Shioaji API action format. Review the place_entry_order method signature to ensure the correct action value is passed.

Suggested change
action="Buy" if internal_action == "long_entry" else "Sell",
action=internal_action,

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +29
Tenant,
TenantCredential,
WorkerInstance,
WorkerStatus,
HealthStatus,
CredentialType,
CredentialStatus,
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Import of 'Tenant' is not used.
Import of 'CredentialStatus' is not used.

Suggested change
Tenant,
TenantCredential,
WorkerInstance,
WorkerStatus,
HealthStatus,
CredentialType,
CredentialStatus,
TenantCredential,
WorkerInstance,
WorkerStatus,
HealthStatus,
CredentialType,

Copilot uses AI. Check for mistakes.
AuditAction,
)
from credentials.encrypted_storage import get_credential_storage
from admin.tenant_service import TenantService, TenantNotFoundError, TenantServiceError
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Import of 'TenantServiceError' is not used.

Suggested change
from admin.tenant_service import TenantService, TenantNotFoundError, TenantServiceError
from admin.tenant_service import TenantService, TenantNotFoundError

Copilot uses AI. Check for mistakes.
# Stop first (ignore if not running)
try:
await manager.stop_worker(tenant_id)
except WorkerNotFoundError:
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except WorkerNotFoundError:
except WorkerNotFoundError:
# Worker was not running; this is expected and safe to ignore when restarting.

Copilot uses AI. Check for mistakes.
container = self._docker.containers.get(instance.container_id)
container.stop(timeout=10)
except NotFound:
pass
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
pass
logger.debug(
"Container %s not found when attempting to stop for hibernation.",
instance.container_id,
)

Copilot uses AI. Check for mistakes.
container = self._docker.containers.get(instance.container_id)
container.remove(force=True)
except NotFound:
pass
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
pass
logger.debug(
"Container %s not found during destroy_worker; assuming already removed",
instance.container_id,
)

Copilot uses AI. Check for mistakes.
@gn00295120
Copy link
Author

gn00295120 commented Jan 2, 2026

here is the frontend website.

https://shioaji-trading-dashboard-saas.pages.dev/

all function is work

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on January 29

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

)
return TenantResponse(**tenant.to_dict())
except TenantNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
Copy link

Choose a reason for hiding this comment

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

Missing tenant ownership checks in admin API endpoints

Multiple admin API endpoints verify the admin token but don't verify tenant ownership. Endpoints like update_tenant, delete_tenant, activate_tenant, suspend_tenant, credential management endpoints, and worker management endpoints only check verify_admin_token without using get_current_user_id to verify the tenant belongs to the requesting user. In contrast, webhook management endpoints correctly check if tenant.owner_id != user_id. This inconsistency allows any authenticated user to modify, delete, or access other users' tenants, credentials, and workers.

Additional Locations (2)

Fix in Cursor Fix in Web

symbol=request.symbol,
position_direction="long" if request.action == "long_exit" else "short",
simulation=simulation,
)
Copy link

Choose a reason for hiding this comment

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

Gateway API passes wrong order parameter values

The gateway's place_order endpoint passes incorrect parameter values to the trading queue. For entry orders, action=request.action passes raw strings like "long_entry"/"short_entry" instead of converting to "Buy"/"Sell" as the existing main.py does. For exit orders, position_direction="long"/"short" is used instead of "Buy"/"Sell". The webhook handler at lines 697-708 correctly converts these values, confirming this is a bug. Orders placed through the gateway API will fail or behave incorrectly because the trading worker expects "Buy"/"Sell" format.

Fix in Cursor Fix in Web

except APIError as e:
instance.status = WorkerStatus.ERROR.value
instance.error_message = str(e)
raise WorkerManagerError(f"Failed to start container: {e}")
Copy link

Choose a reason for hiding this comment

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

Worker error status not persisted when Docker operations fail

In start_worker, stop_worker, and wake_worker, when Docker operations fail (NotFound or APIError), the code sets instance.status to ERROR and instance.error_message before raising an exception. However, the exception causes execution to skip past db.commit(), so these status updates are never persisted. Subsequent calls to get_worker_status will show stale status instead of the actual error state, making it difficult for users to diagnose why their worker isn't running.

Additional Locations (2)

Fix in Cursor Fix in Web

cred_files = storage.export_for_worker(str(tenant_id))

if not cred_files:
raise CredentialsNotFoundError(f"Failed to export credentials for tenant {tenant.slug}")
Copy link

Choose a reason for hiding this comment

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

Early commit in create_worker leaves orphaned instances blocking retries

When creating a new worker, create_worker_instance commits the database transaction immediately (line 638 in tenant_service.py). If subsequent operations fail—such as export_for_worker returning empty or Docker container creation failing—the worker instance remains in the database with status PENDING and no container_id. On the next create_worker call, the check at line 172 sees status PENDING (which is not in "stopped" or "error") and raises WorkerAlreadyRunningError. The tenant becomes permanently stuck, unable to create a worker without manual database cleanup.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant