← Selected work
Case study · Security SaaS

A multi-tenant security testing platform, built solo in 8 weeks.

Continuous attack-surface discovery, distributed vulnerability scanning, and client-ready reporting — on a security-grade microservice backend. Here is the architecture and the decisions behind it, not just the feature list.

Architecture

5 services + isolated task workers behind Nginx

The hard part

Tenant isolation & scan orchestration that actually hold

Proof

Real scans, real reports, a real deploy — solo

What & Why

Skylance's own product — built to prove one claim.

That one AI-native engineer can ship a real, security-grade, multi-tenant SaaS — the kind of system that usually takes a funded team.

It continuously discovers an organization's internet-facing assets, scans them for vulnerabilities, and turns the findings into client-ready security reports — automatically. Attack-surface management is a domain where "roughly right" isn't good enough: multi-tenant isolation, auth hardening, and correct scan pipelines have to actually work.

Constraint that shaped everything: solo, on a single modest server. That budget is exactly why the interesting decisions below are about isolation, throughput, and cost — not about throwing hardware at the problem.

System design

Eleven containers, five services, two isolated worker pools.

The topology is microservice-style on one host: five FastAPI services handle auth, business API, policies, registration, and AI translation; two Celery worker pools (one for execution, one exclusively for monitoring) sit behind Redis queues; PostgreSQL and Redis anchor the data and coordination layers. The front-end is a React SPA on Cloudflare Pages; Nginx reverse-proxies everything.

Architecture topology
User browser
↓ HTTPS
Cloudflare — CDN · WAF · React front-end (Pages)
Nginx (reverse proxy, TLS termination)
↓ splits to 5 services
auth :8001
api :8002 (assets/vulns/tenants)
policytask :8003 (scan policies)
register :8100
aipoint :9001 (AI translation)
Redis — task queues · sessions · brute-force cnt
Celery main worker (c=2)
4 queues: default · asset · vuln · report
Celery monitoring worker (c=1)
dedicated queue · 60s Nessus poll
PostgreSQL 16 (127.0.0.1 only)
External: Nessus scanner · FOFA discovery API

Five services, two worker pools, one scheduler, Postgres and Redis — eleven containers total.

Trade-offs

Four decisions, with the reasoning and the cost made explicit.

Decision 1

Isolate monitoring from execution at the queue level

Problem

Nessus status polling runs every 60 seconds and must stay responsive. Asset and vulnerability scans run for 30–45 minutes. On one shared queue, a burst of long scans starves the polling that's supposed to be watching them.

Options

One `default` queue for everything (simple, but head-of-line blocking) · A dedicated `monitoring` queue with its own worker

Choice

A separate `monitoring` queue on a single-concurrency worker, with task priorities layered on top. Long scans and fast polling never compete.

Cost

A second worker process (~300 MB RAM) and hand-maintained routing for every task type — eleven task families total.

Evidence: policytask/celery_app.py
Decision 2

Make scans fast without missing services

Problem

Scanning a fixed 1–10,000 port range tripped Nessus's "Max Ports Exceeded" limit on WAF/CDN-fronted targets, and those scans dragged past 45 minutes. Scanning only the asset's known ports would be fast but blind to anything newly exposed.

Options

Fixed wide range (slow, error-prone) · Known-ports-only (fast, blind) · Computed union with adaptive grouping

Choice

Scan `known ports ∪ a 46-port baseline`. When the union exceeds 128 ports, split with greedy bin-packing and run groups concurrently.

Cost

Multi-scan-ID bookkeeping and ~200 lines of grouping logic. Coverage now depends on asset-data freshness.

Result (measured)

On protected targets: ~45 min → 12–18 min (≈60% faster). Tasks hitting "Max Ports Exceeded": ~30% → <5%.

Evidence: 扫描加速实施计划.md · nessus_executor.py
Decision 3

Put tenant isolation in the query, on purpose

Problem

Every asset, vulnerability, and report row belongs to a tenant. One missed filter leaks another tenant's attack surface — the worst possible bug for a security product.

Options

PostgreSQL Row-Level Security (RLS) · Schema-per-tenant · Explicit WHERE uid/tid in application layer

Choice

Explicit application-layer filtering — every query carries uid/tid derived from JWT, with contract tests that probe for cross-tenant leakage.

Cost (stated plainly)

~140 query sites that each must remember the filter — a real review burden, mitigated by the contract test.

Evidence: api/tenant/service.py · api/asset/service.py
Decision 4

Local Postgres over managed, as a deliberate cost trade

Problem

The database could be a managed service (Supabase) or a local Postgres container on the existing host.

DimensionLocal PGManaged (Supabase)
Monthly cost$16–27$34–47
Latency<1 ms1–3 ms
Failure RPO≤24 hours0 (PITR)
Long transactionsunlimited5-minute pooler cap
Choice & Cost

Local Postgres 16, bound to 127.0.0.1, with self-built pg_dump → Cloudflare R2 daily backup. Saves ~$220/year and keeps sub-ms latency. The cost: I own backups and the RPO is ≤24h instead of zero — an explicit, documented trade.

Evidence: 生产环境架构设计.md
Code & proof

The engineering, not just the story.

Artifact A — The enforced response envelope
class ApiResponse(BaseModel):
    code: int = 0
    message: str = "success"
    data: Optional[Any] = None
    success: bool = True

def error_response(code: int, message: str, data=None):
    return ApiResponse(code=code, message=message, 
                      data=data, success=False)

49 error codes, enforced by a contract test — so the front-end has exactly one way to handle success and failure.

Artifact B — Auth hardening, in brief

Argon2 password hashing (memory-hard), a prepared-login handshake that puts Turnstile and rate-limiting before any password check, exponential lockout (60s → 300s → 3,600s → 86,400s → 1 year), max 5 sessions per user, and revocable JWTs with jti.

By the numbers
Measured
  • · 132 findings on a reference scan — 18 Critical / 57 High / 44 Medium / 13 Low
  • · Scan acceleration: 45 min → 12–18 min (60% faster), error rate 30% → <5%
  • · 5 services · 2 worker pools · 11 containers · 78 API endpoints · 49 error codes
Designed for (capacity)
  • · 10+ concurrent tenants
  • · Per-tenant capacity: 2,000 monitored assets, 20,000 asset-scans and 1,000 vulnerability-scans per month
The takeaway

This is what "AI-native, production-grade" actually means.

A funded team's worth of security infrastructure — architecture, async processing, hardened auth, real deployment — shipped solo, and transparent enough that you can check the reasoning, not just the result.

If you need a real SaaS or security platform built, not a prototype, this is the bar.