When running a digital Shop, your storefront is your first impression. While platforms like Shopify or WooCommerce are great for standard retail, selling digital solutions, custom software packages, and retainers often require a highly tailored checkout flow.
Instead of fighting against the constraints of off-the-shelf website builders, I decided to build the Duncan Digital Solutions e-commerce platform entirely from scratch.
Because this is the proprietary engine running my live business, the GitHub repository remains private. However, I want to pull back the curtain and share the architectural decisions, NoSQL data modeling, and security protocols I used to build a lightning-fast, serverless storefront using Angular and Firebase.
The Tech Stack
To ensure the platform was cost-effective, highly scalable, and real-time, I went with a fully serverless architecture:
- Frontend: Angular (utilizing Signals for reactive state management)
- Authentication: Firebase Auth (Google & Email/Password)
- Database: Cloud Firestore (NoSQL document database)
- Backend Logic: Firebase Cloud Functions (Node.js)
- Hosting: Firebase Hosting with CDN caching
NoSQL Data Modeling for E-Commerce
Moving from a traditional SQL relational database to Firestore’s NoSQL structure requires a shift in mindset. You don't have SQL JOIN operations, so data must be structured for fast reads.
Here is how I structured the three core pillars of the platform: Users, Products, and Orders.
1. The Products Collection
Instead of heavily nesting data, products are kept flat so they can be queried instantly on the storefront.
// Collection: /products/{productId}
{
"name": "Custom Angular Web App",
"category": "Development",
"price": 50000,
"currency": "KES",
"isActive": true,
"features": ["Responsive Design", "Firebase Backend", "Admin Dashboard"],
"createdAt": "Timestamp"
}
2. The Orders Collection (Denormalization)
In NoSQL, it is better to duplicate a little data than to require multiple reads. When a user checks out, I copy the product details into the order document. This ensures that if I change the price of a product next year, the historical order receipt remains completely accurate.
// Collection: /orders/{orderId}
{
"userId": "auth_uid_123",
"customerEmail": "client@example.com",
"status": "PROCESSING", // PENDING, PROCESSING, COMPLETED
"totalAmount": 50000,
"items": [
{
"productId": "prod_889",
"name": "Custom Angular Web App",
"priceAtPurchase": 50000
}
],
"createdAt": "Timestamp"
}
Bulletproof Security with Firestore Rules
Because the frontend talks directly to the database, security is the most critical part of this architecture. A malicious user could theoretically open the browser console and try to read other people's orders or change a product price to $0.
To prevent this, I wrote strict Firestore Security Rules that act as an impenetrable firewall.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 1. PRODUCTS: Anyone can read, only the Admin can create/update/delete
match /products/{productId} {
allow read: if true;
allow write: if request.auth != null && request.auth.token.admin == true;
}
// 2. ORDERS: Users can only read and create their OWN orders
match /orders/{orderId} {
// User must be logged in and the ID on the order must match their Auth ID
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
// When creating an order, ensure they aren't spoofing the userId
allow create: if request.auth != null && request.resource.data.userId == request.auth.uid;
// Only Admin can update the order status (e.g., from PENDING to COMPLETED)
allow update, delete: if request.auth != null && request.auth.token.admin == true;
}
}
}
By enforcing request.auth.uid == resource.data.userId, the database physically rejects any attempt by a user to fetch an order receipt that doesn't belong to them.
Reactive Cart State with Angular Signals
Managing shopping cart state across multiple components (the navbar cart icon, the product page, and the checkout drawer) used to require complex RxJS BehaviorSubjects. By leveraging modern Angular Signals, the cart logic became incredibly clean and synchronous.
import { Injectable, signal, computed } from '@angular/core';
import { Product } from '../models/product.model';
@Injectable({ providedIn: 'root' })
export class CartService {
// The core reactive state
private cartItems = signal<Product[]>([]);
// Computed signals automatically update when cartItems changes
readonly items = this.cartItems.asReadonly();
readonly cartCount = computed(() => this.cartItems().length);
readonly cartTotal = computed(() =>
this.cartItems().reduce((total, item) => total + item.price, 0)
);
addToCart(product: Product) {
this.cartItems.update(items => [...items, product]);
}
removeFromCart(productId: string) {
this.cartItems.update(items => items.filter(i => i.id !== productId));
}
}
Now, binding the cart total in the navbar is as simple as {{ cartService.cartTotal() }}, and Angular's change detection handles the rest with zero performance overhead.
Conclusion
Building a custom e-commerce solution is not for the faint of heart, but the level of control it gives your business is unmatched. By combining Angular's strict, component-driven architecture with Firebase's scalable, serverless backend, the Duncan Digital Solutions platform is fast, secure, and perfectly tailored to sell high-end digital services.
If you are an online shop owner, taking the time to build your own infrastructure is one of the best investments you can make in your brand's technical authority.
Website link [https://duncandigitals.com/]
Top comments (0)