- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Object Storage Explained: S3 Buckets, Keys and Signed URLs Without the Confusion
About Post
Your app stores uploads in storage/app. Then you add a second server behind a load balancer, and half your users start getting 404s on files they uploaded five minutes ago. The file exists. It's just on the other server.
That's usually the day a team moves to object storage, and Amazon S3 is the name everyone knows. It's also the day a new set of confusions begins, because S3 looks like a hard drive in the cloud. It isn't one, and almost every S3 mistake comes from treating it like one.
The mental model: a giant coat check
A file system is a filing cabinet. There are drawers inside drawers (folders), you can open a file and change a page, and you can move a folder by dragging it.
Object storage is a coat check. You hand over a coat (the object), you get a ticket (the key), and when you show the ticket, you get the whole coat back. You can't swap a button while it's hanging there. You can only hand in a new coat under the same ticket.
In S3 terms:
- A bucket is the coat check itself: a container with a globally unique name, living in one AWS region.
- A key is the ticket: a string like
contracts/2026/03/8f2c9a.pdf. - An object is the data plus its metadata (content type, size, custom headers).
Surprise 1: folders don't exist
That key above has slashes in it, and the AWS console shows it inside folders. But S3's namespace is flat. The slashes are just characters in the key, and "folders" are a display trick based on shared prefixes.
This has practical consequences:
- Renaming a "folder" isn't one operation. It means copying every object to a new key and deleting the old one. Choose your key structure carefully up front.
- Listing is by prefix. "Give me everything under
contracts/2026/" is cheap and natural. "Find every PDF anywhere" means listing everything and filtering yourself. - Empty folders can't really exist. When a tool creates one, it's actually a zero-byte object with a key ending in
/.
Surprise 2: you can't edit an object
There's no "open the file and append a line". Every write replaces the whole object. That's why S3 is wonderful for uploads, PDFs, images, backups and exports, and the wrong place for a log file you append to or a SQLite database.
The good news: since late 2020, S3 gives strong read-after-write consistency. Once a write succeeds, the next read returns the new version. Older blog posts that warn about "eventual consistency" are describing a problem that no longer exists.
| File system | Object storage (S3) | |
|---|---|---|
| Structure | Real nested folders | Flat keys; folders are prefixes |
| Editing | Change part of a file | Replace the whole object |
| Access | Through the OS, one machine | Over HTTP, from anywhere |
| Scaling | Limited by the disk | Effectively unlimited |
| Best for | Code, temp files, local cache | Uploads, documents, media, backups |
Surprise 3: private is the default, and that's good
New S3 buckets block public access by default. Developers sometimes see this as an obstacle ("my image URL returns Access Denied!") and switch it off. Please don't, at least not for anything a user uploaded.
Think about what lives in a typical business app's storage: signed contracts, ID copies, invoices, payslips. If those sit in a public bucket, the only thing protecting them is that nobody has guessed the URL yet. URLs get pasted into emails, logged by proxies and kept in browser history. That's not security.
So keep the bucket private. Which raises the obvious question: how does a user download their own file?
Signed URLs: the valet key for one file
A presigned URL is a normal S3 URL with a signature in the query string. Your server creates it using its own AWS credentials, and it grants one specific permission (read this object, or upload to this key) until a set expiry time.
The browser fetches the file directly from S3. Your PHP process never touches the bytes, which matters a lot when the file is a large PDF or a video.
A few facts worth knowing:
- Signing happens locally. Generating a URL doesn't call AWS; it's a cryptographic calculation, so it's fast.
- The URL only carries the permissions of the credentials that signed it. If your IAM role can't read the object, neither can the URL.
- If you sign with temporary credentials (for example, an EC2 instance role), the URL stops working when those credentials expire, even if you asked for a longer lifetime.
- Anyone holding the URL can use it until it expires. Keep the lifetime short: minutes, not days.
The AWS docs on presigned URLs go into the details.
Doing it in Laravel
Laravel's filesystem wraps all of this nicely. Install the S3 adapter (composer require league/flysystem-aws-s3-v3 "^3.0"), set the AWS_* values in .env, and use the s3 disk.
// Store an upload under a key you control
$key = $request->file('document')->store("contracts/{$contract->id}", 's3');
$contract->update(['document_key' => $key]);
// Later: authorise first, then hand out a short-lived link
public function download(Contract $contract)
{
$this->authorize('view', $contract);
return redirect(Storage::disk('s3')->temporaryUrl(
$contract->document_key,
now()->addMinutes(5),
['ResponseContentDisposition' => 'attachment; filename="contract.pdf"'],
));
}
Notice what's happening: your app still decides who may download (the policy check), and S3 does the heavy lifting of serving it. The ResponseContentDisposition option makes the browser download the file with a friendly name instead of showing a random key.
For large uploads, the same idea works in reverse. Storage::disk('s3')->temporaryUploadUrl($key, now()->addMinutes(5)) returns a URL and headers that let the browser or mobile app PUT the file straight to S3. Your server only records the key once the upload is done. The filesystem docs cover both methods.
Store the key, not the URL. Save contracts/42/abc.pdf in your database, and generate a fresh signed URL each time someone needs it. Stored URLs expire, break when you change buckets or CDNs, and leak access if they ever get copied.
The mistakes I see most
- Using the user's file name as the key. Names collide, contain odd characters and sometimes reveal information. Let Laravel generate a random name and keep the original name in a database column if you need it.
- Making the bucket public "just for images". Use a separate public bucket (or a CDN in front) for truly public assets, and keep user documents in a private one.
- Proxying downloads through PHP. Streaming a file from S3 to your server and then to the user doubles the work and ties up a PHP worker. Redirect to a signed URL instead.
- Week-long signed URLs. If a link needs to work next week, generate it next week.
- Forgetting it's not only AWS. Cloudflare R2, DigitalOcean Spaces and MinIO speak the same S3 API, so everything here applies, and MinIO is handy for local development.
In short
Object storage is a coat check, not a filing cabinet. Keys are tickets, folders are an illusion, objects are replaced rather than edited, and private files stay private while signed URLs hand out temporary, single-file access.
Get those four ideas right and S3 becomes one of the most boring, reliable parts of your stack, which is exactly what storage should be.
How do you structure your S3 keys: by user, by date, by record ID, or something else? I'm curious what has held up best for you over time.

Be first to comment it...