From localhost friction to production-shaped architecture
A case study from Parjanya v2.0: multi-tenant S3, IAM isolation, and server-to-server architecture patterns
Lessons from Parjanya v2.0 and the v5.4 architecture revision
I never treated localhost as the target architecture. It was simply the default starting point for local development, as it is for most full-stack systems. But as I built Parjanya v2.0, I kept running into a recurring class of failures: CORS errors, origin mismatch, presigned upload friction, and the widening gap between development-time assumptions and production-time behaviour. That experience forced me to step back and re-evaluate the system from first principles. What emerged was not a localhost fix, but a stronger production-shaped model: server-to-server architecture for cloud-based full-stack applications and distributed systems.
Why localhost stopped being enough
Local development is useful because it is fast. But speed can hide architecture debt.
In Parjanya v2.0, the real problem was not that localhost was broken. The problem was that localhost encouraged a development loop that was too far removed from the actual production trust boundary. The browser, S3, IAM, and backend services each enforce different rules, and local development often smooths those rules away just enough to create false confidence. Then the system reaches the real deployment path and the hidden assumptions come apart.
That became especially clear with browser-based uploads. A local setup may appear functional, but the moment the frontend starts speaking directly to tenant-specific buckets in S3, the browser becomes an active participant in the security model. The result is not just a technical inconvenience; it is a signal that the architecture itself needs to be shaped more deliberately.
The first real signal: CORS was exposing the architecture gap
CORS was one of the clearest signs that the local development experience was drifting away from the real system.
In theory, CORS is straightforward. In practice, it is one of the fastest ways to reveal that the frontend and backend are too loosely coupled. In Parjanya, browser uploads to S3 were not talking to a generic bucket. They were talking to tenant-scoped storage with strict policy boundaries. That meant every origin, method, and header mattered. A local proxy or permissive dev configuration might make the request succeed temporarily, but that success did not mean the architecture was correct. It only meant the browser had not yet been forced to tell the truth.
The architectural implication was obvious: if the browser has to fight the security model just to upload a file, the development workflow is revealing a deeper mismatch. In my case, that mismatch was not merely about CORS syntax. It was about the location of trust.
What Parjanya v2.0 architecture v5.4 actually changed
Architecture v5.4 is an architecture revision inside Parjanya v2.0, not a separate product. That revision was shaped by one principle: move trust out of the browser and into the backend.
The S3 architecture notes show the core design clearly: one bucket per tenant, IAM-scoped permissions, bucket policies, presigned URL generation in the backend, and lifecycle rules for cost control. The model is intentionally infrastructure-enforced. Tenant isolation is not left to frontend conventions or prefix hygiene. It is built into the storage boundary itself.
That design changes the system in a fundamental way. Instead of letting the frontend improvise access, the backend becomes the authority that validates identity, determines tenant ownership, constructs the object key, and issues a time-bound signed capability. The browser no longer owns authorization. It simply executes what the backend has already allowed.
Why server-to-server architecture made more sense
Once I looked at the problem that way, the answer became much clearer.
For a cloud-native application, especially a multi-tenant one, the frontend should not be the place where trust decisions live. The frontend should be delivered through a stable edge layer, the backend should own policy and identity, and object storage should be accessed through signed backend-issued capabilities. That is why I started favouring a server-to-server model for real production systems.
In practice, that means serving the frontend from CloudFront, exposing backend services through ALB, and letting the backend own S3 access through presigned URLs. The browser becomes a client, not a policy engine. The backend becomes the control point. The trust boundary becomes explicit. And the deployment path starts to look like the real system instead of a local approximation.
That shift also made debugging easier. When a request fails, there are fewer places to guess. Is it a policy issue? A bucket issue? A signature issue? A CORS issue? A routing issue? With a server-to-server design, the failure surface becomes narrower and more legible.
Multi-tenant storage is an infrastructure problem, not just an application problem
One of the strongest lessons from Parjanya’s architecture revision was that multi-tenant storage cannot be treated as an application afterthought.
The architecture notes show a per-tenant bucket model with explicit role and bucket policy enforcement. That is the right direction because it reduces ambiguity. Tenant isolation should not depend entirely on code discipline or naming conventions; it should be enforced by the infrastructure boundary itself.
This matters because multi-tenant systems fail in subtle ways when storage is shared too casually. A shared bucket can work, but it increases the burden on prefix logic, object ownership rules, and application-layer checks. A per-tenant bucket model is cleaner to reason about and easier to audit. It also aligns better with the way the rest of the Parjanya stack is already designed: backend validation, signed upload paths, explicit bucket policies, and lifecycle management.
Presigned URLs are capability tokens, not shortcuts
Presigned URLs were another major lesson.
A presigned URL is not just a convenience feature. It is a capability token that allows a very specific action for a very specific scope and time window. That makes it a very strong fit for Parjanya because the backend can validate the request first and then issue a constrained upload path. The browser does not need broad credentials. It only needs the signed permission to perform the exact operation the backend allowed.
That is the right security model for a SaaS system with tenant isolation. It reduces exposure, tightens the trust boundary, and lets the backend remain the source of truth for who can upload what and where. It also makes the system far easier to evolve because the upload flow is now a controlled protocol, not a loose browser-side improvisation.
Production-shaped development is the real goal
The biggest shift in my thinking was this: the goal is not to make localhost feel like production forever. The goal is to make local development reveal the same architectural boundaries that production will enforce.
That means treating origin boundaries, identity boundaries, and policy boundaries as first-class concerns from the beginning. It means avoiding development shortcuts that erase those boundaries too early. It means designing the system so that local development is still useful, but not misleading. And it means accepting that some friction is not a bug in the workflow — it is the architecture trying to tell you something.
In Parjanya v2.0, that lesson showed up everywhere: in CORS behavior, in presigned URL flows, in per-tenant bucket design, and in the decision to move toward a cleaner CloudFront + ALB + backend-controlled storage path. The architecture revision in v5.4 was not just an optimization. It was a correction toward a more honest production model.
What I would tell another engineering team
If I were explaining this to another team building a cloud-native product, I would say this:
localhost is fine for iteration, but it should not define the architecture. For distributed systems, especially multi-tenant SaaS, the real system lives in the boundaries: frontend delivery, backend authority, object storage policy, and signed capabilities. If those boundaries are blurred during development, the application will pay for it later in debugging time, security risk, and operational complexity.
The better pattern is production-shaped from the start:
the frontend should be served from CloudFront, backend services should sit behind ALB, the backend should generate presigned URLs, and S3 access should be governed by infrastructure-enforced tenant isolation. That design is cleaner, more secure, and much closer to how real cloud applications behave in production.
Closing thought
Parjanya v2.0 taught me that architecture is often revealed through friction.
CORS failures were not just browser noise. They were evidence that trust boundaries needed to be sharper. Presigned URLs were not just an implementation detail. They were the correct way to move capability into the backend. Per-tenant buckets were not just a storage choice. They were the cleanest way to enforce isolation. And localhost, while useful, was never the architecture — only the starting point.
The end result is simple: I now think in terms of production-shaped systems, not localhost-shaped workflows. For me, that is the difference between a prototype that merely works and a system that is actually ready to scale.
Good catch — here’s a clean, copy-paste ready References section with actual links (kept high-signal and aligned to your architecture themes).
📚 References & Further Reading
☁️ AWS (Core to this architecture)
Multi-tenant S3 design patterns (must read)
AWS Storage Blog
https://aws.amazon.com/blogs/storage/design-patterns-for-multi-tenant-access-control-on-amazon-s3/Dynamic routing with CloudFront (multi-tenant edge patterns)
AWS Architecture Blog
https://aws.amazon.com/blogs/architecture/dynamic-request-routing-in-multi-tenant-systems-with-amazon-cloudfront/CloudFront + origin patterns (edge → backend)
AWS Docs
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.htmlRestricting ALB access via CloudFront (edge-controlled backend)
AWS Docs
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/restrict-access-to-load-balancer.htmlPresigned URLs (backend-controlled access model)
AWS Docs
https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.htmlCORS troubleshooting (why browser uploads fail)
AWS Docs
https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors-troubleshooting.htmlAWS Well-Architected Framework
https://aws.amazon.com/architecture/well-architected/
These patterns are consistent across modern cloud-native systems:
move trust to the backend, enforce isolation at the infrastructure layer, and design for production boundaries early.

