Back to Portfolio
CUSTOM FULL-STACK BUILD

Quire
Bookstore

quire (n.) — a gathering of folded pages; the fundamental unit from which every book is assembled. Twenty-five sheets, folded together, become something greater.

A complete mobile commerce ecosystem — from curated book browsing to secure checkout.Flutter mobile app with Clean Architecture. Node.js backend with Domain-Driven Design.Real Stripe payments. Built to scale.

0+
Screens
0
Backend Modules
30-45
Day Delivery
$12k
Investment
The Brief

How It Started

Real communication. Real requirements. Real solution.

I'm opening an independent bookstore and I need a mobile app — not just a catalog, but a real storefront. Something that competes with the big guys. I want my customers to browse, add to cart, and actually check out with real payments. And I need a backend that won't fall apart when I scale.

That's exactly what we specialize in. We'll build the mobile app in Flutter with Clean Architecture — it keeps the codebase modular and testable, so when you grow from 30 books to 30,000, the app doesn't need a rewrite. For the backend, we'll use a DDD modular monolith in Node.js — same principle, each business domain is self-contained.

I want real payments — Stripe or something reliable. And the experience needs to feel premium. My customers are book lovers, not tech people. It has to feel like walking into their favorite bookshop, not wrestling with a clunky app.

Stripe is exactly what we'll wire in — real PaymentIntent flows with webhook confirmation, not a mock. The UI will be a warm dark theme with amber accents, built to feel intimate and curated. We'll use BLoC for state management so every interaction — adding to cart, checking out, tracking orders — feels instant and smooth.

What about maintenance? I don't want something that breaks every time I add a category or a new payment option. And if I hire another developer later, they shouldn't need a month to understand the code.

That's the whole point of Clean Architecture on the Flutter side and DDD on the backend. Every feature is a self-contained module. A new developer opens the books/ folder and sees the entity, the repository, the BLoC, the UI — all in one place. Adding a "reviews" feature means adding a new module, not touching existing ones. The backend mirrors this exactly — 5 bounded contexts, each with its own domain, handlers, and routes.

OK, this sounds like what I need. What's the timeline, and do I get the source code?

30 to 45 days, and yes — you get everything. Both the Flutter source code and the full Node.js backend, the database migrations, API docs. It's yours. You own it. We include 30 days of post-delivery support to make sure everything runs smooth in production.

The Challenge

Competing Against Giants With Zero Digital Presence

Independent bookstores are invisible to mobile-first customers who default to Amazon

No way to browse inventory, check availability, or purchase outside store hours

Paper-based operations can't track customer preferences or build loyalty

Generic website templates feel impersonal and offer no competitive edge

Our Solution

A Personal Bookshop in Every Customer's Pocket

A polished mobile storefront with curated categories, search, and discovery features

Real-time cart with tax and shipping calculation, powered by Stripe secure checkout

JWT-authenticated user accounts with order history and personalized experience

Architecture built to evolve — add reviews, wishlists, or recommendations without rewriting

Features That Sell

Every feature engineered with both the user experience and the codebase in mind.

Curated Book Browsing

Categories, filters, and search across 30+ titles. Rich book detail pages with ratings, descriptions, and pricing.

Real-Time Cart

Add, remove, adjust quantities with instant UI feedback. Tax calculation, shipping estimation, and coupon support.

Stripe Checkout

Real PaymentIntent flows with webhook confirmation. Secure card processing, not a mock or simulation.

JWT Authentication

Secure login and registration with bcrypt hashing. Access + refresh token rotation. Protected routes.

Order Tracking

Complete order history with status progression. From pending to delivered, every state visible in real time.

Smart Search

Filter by category, price range, and rating. Full-text search across titles and authors.

Hive Caching

Offline-ready cart persistence. Favorites and wishlist survive app restarts. Instant data loading.

Amber Codex Theme

Custom dark theme with warm amber accents. Glassmorphism cards, animated transitions, premium feel.

Results That Matter

Strong Digital Brand

A polished, premium app that positions the bookstore as a modern destination — not just another shop

24/7 Mobile Storefront

Customers browse and buy at midnight, on the bus, or during lunch — the store never closes

Seamless Purchase Flow

From discovery to checkout in under 60 seconds. Cart, tax, shipping, payment — all in one smooth experience

Scalable Architecture

Add 10,000 books, a recommendation engine, or a loyalty program — the modular structure absorbs growth

Google Play Publishing

We Handle
the Launch Too.

Don't stress about rejections, signing keys, or privacy policies. We handle the full Google Play submission pipeline — so you ship on time, not after three rounds of review hell.

Keystore generation, signing & secure backup
GDPR-compliant privacy policy (written & hosted)
Store listing: screenshots, feature graphic, descriptions
Release track management: internal → alpha → production
AAB build, sign & upload via Fastlane
Review response & resubmission if Google rejects

One thing on your side: make sure your company has a DUNS number. Google requires it for organisation developer accounts — and getting one can take 2–4 weeks if you don't already have it.

// google play publishing is included in all full-stack projects

fastlane supply --track production
Live
Generating upload keystore...
keystore.jks created & encrypted
key.properties configured in gradle
Writing privacy policy...
GDPR-compliant policy generated
Hosted at /privacy
Preparing store listing...
Screenshots (phone + 7-inch tablet)
Feature graphic (1024×500px)
Short & full descriptions written
Building & uploading via Fastlane...
AAB signed with upload key
Track: internal → alpha → production
Awaiting Google review...
Policy review: passed
Content rating: passed
Target API level: passed
🚀Status: Live on Google Playelapsed 04:37

* representative output from a real project deployment

Mobile App

Flutter Under the Hood

Clean Architecture with BLoC pattern. Every layer has a purpose. Every dependency points inward.

Clean ArchitectureBLoC PatternGetIt DIgo_routerHive StorageEquatabledartz

Project Structure

lib/
lib/
├── config/
│ ├── routes/app_router.dart
│ ├── theme/app_theme.dart
│ └── dependency_injection.dart
├── core/
│ ├── data/mock_data_service.dart
│ ├── errors/failures.dart
│ └── network/
├── features/
│ ├── auth/
│ │ ├── data/datasources/
│ │ ├── domain/usecases/
│ │ └── presentation/bloc/
│ ├── books/
│ │ ├── data/repositories/
│ │ ├── domain/entities/book.dart
│ │ └── presentation/
│ │ ├── bloc/books_bloc.dart
│ │ ├── pages/
│ │ └── widgets/
│ ├── cart/
│ │ ├── data/datasources/cart_local_datasource.dart
│ │ ├── domain/entities/cart_item.dart
│ │ └── presentation/bloc/cart_bloc.dart
│ ├── favorites/
│ ├── orders/
│ └── wishlist/
└── shared/widgets/

Code Highlights

— real excerpts from the codebase
features/books/domain/entities/book.dart
dart171L
class Book extends Equatable {
  final String id;
  final String title;
  final String author;
  final double price;
  final double? originalPrice;
  final double rating;
  final List<String> categories;
  final bool inStock;

  const Book({
    required this.id,
    required this.title,
    // ... 17 properties total
  });

  bool get hasDiscount =>
    originalPrice != null && originalPrice! > price;

  double get discountPercentage {
    if (!hasDiscount) return 0;
    return ((originalPrice! - price) / originalPrice! * 100);
  }

  @override
  List<Object?> get props => [id, title, author, ...];
}
features/cart/presentation/bloc/cart_bloc.dart
dart266L
class CartBloc extends Bloc<CartEvent, CartState> {
  final GetCart getCart;
  final AddToCart addToCart;
  final RemoveFromCart removeFromCart;
  final UpdateCartItem updateCartItem;
  final ClearCart clearCart;

  CartBloc({required this.getCart, ...})
    : super(const CartInitial()) {
    on<LoadCart>(_onLoadCart);
    on<AddBookToCart>(_onAddToCart);
    on<RemoveItemFromCart>(_onRemoveFromCart);
    on<IncrementItemQuantity>(_onIncrementQuantity);
    on<DecrementItemQuantity>(_onDecrementQuantity);
  }

  Future<void> _onAddToCart(AddBookToCart event, Emitter emit) async {
    final result = await addToCart(event.book, quantity: event.quantity);

    result.fold(
      (failure) => emit(CartError(failure)),
      (item) {
        emit(CartItemAdded(item));  // Optimistic UI feedback
        add(const LoadCart());     // Reload fresh state
      },
    );
  }
}
config/dependency_injection.dart
dart339L
final sl = GetIt.instance;

Future<void> initDependencies() async {
  await Hive.initFlutter();
  await Hive.openBox(AppConstants.cartBox);
  await Hive.openBox(AppConstants.cacheBox);

  _initExternal();    // FlutterSecureStorage, Connectivity
  _initCore();        // NetworkInfo, ApiClient
  _initAuthFeature();  // Auth BLoC → UseCases → Repo → DataSources
  _initBooksFeature();
  _initCartFeature();
  _initOrdersFeature();
}

void _initCartFeature() {
  // BLoC — factory (new instance per widget tree)
  sl.registerFactory(() => CartBloc(
    getCart: sl(), addToCart: sl(),
    removeFromCart: sl(), updateCartItem: sl(), clearCart: sl(),
  ));

  // Use cases — singleton (stateless)
  sl.registerLazySingleton(() => GetCart(sl()));
  sl.registerLazySingleton(() => AddToCart(sl()));

  // Repository — singleton (single source of truth)
  sl.registerLazySingleton<CartRepository>(
    () => CartRepositoryImpl(localDataSource: sl()),
  );

  // DataSource — Hive box for local persistence
  sl.registerLazySingleton<CartLocalDataSource>(
    () => CartLocalDataSourceImpl(
      cartBox: Hive.box(AppConstants.cartBox),
    ),
  );
}
config/routes/app_router.dart
dart
final authBloc = sl<AuthBloc>();

final router = GoRouter(
  refreshListenable: GoRouterRefreshStream(authBloc.stream),
  redirect: (context, state) {
    final isLoggedIn = authBloc.state is AuthAuthenticated;
    final isAuthRoute = state.matchedLocation.startsWith('/login');

    if (!isLoggedIn && !isAuthRoute) return '/login';
    if (isLoggedIn && isAuthRoute)  return '/';
    return null;
  },
  routes: [
    ShellRoute(
      builder: (context, state, child) => MultiBlocProvider(
        providers: [
          BlocProvider(create: (_) => sl<BooksBloc>()),
          BlocProvider(create: (_) => sl<CartBloc>()),
          BlocProvider(create: (_) => sl<FavoritesBloc>()),
        ],
        child: ScaffoldWithNavBar(child: child),
      ),
      routes: [ /* nested routes... */ ],
    ),
  ],
);
features/cart/data/datasources/cart_local_datasource.dart
dart
class CartLocalDataSourceImpl implements CartLocalDataSource {
  final Box cartBox;

  CartLocalDataSourceImpl({required this.cartBox});

  @override
  Future<CartItemModel> addToCart(Book book, {int quantity = 1}) async {
    // Deduplicate: increment if already exists
    final existing = cartBox.values.cast<String>()
      .map((json) => CartItemModel.fromJson(jsonDecode(json)))
      .where((item) => item.book.id == book.id);

    if (existing.isNotEmpty) {
      final updated = existing.first.copyWith(
        quantity: existing.first.quantity + quantity,
      );
      await cartBox.put(updated.id, jsonEncode(updated.toJson()));
      return updated;
    }

    final item = CartItemModel.create(book: book, quantity: quantity);
    await cartBox.put(item.id, jsonEncode(item.toJson()));
    return item;
  }
}
Backend API

Node.js Under the Hood

Domain-Driven Design. Five bounded contexts. Each module owns its data, logic, and routes.

TypeScriptExpressMariaDBPinoStripe SDKZodbcrypt + JWTdb-migrate

Backend Structure

— 75 source files, 9 database migrations
src/
src/
├── modules/
│ ├── auth/
│ │ ├── domain/entities/User.ts
│ │ ├── domain/repositories/IAuthRepository.ts
│ │ ├── domain/services/AuthDomainService.ts
│ │ ├── application/commands/LoginCommand.ts
│ │ ├── application/handlers/AuthHandler.ts
│ │ ├── infrastructure/crypto/TokenService.ts
│ │ ├── infrastructure/persistence/AuthRepository.ts
│ │ ├── presentation/routes/auth.routes.ts
│ │ └── auth.module.ts
│ ├── books/
│ ├── orders/
│ ├── payments/
│ └── users/
├── shared/
│ ├── kernel/
│ │ ├── Result.ts
│ │ ├── EventBus.ts
│ │ └── Container.ts
│ ├── database/
│ │ ├── ConnectionPool.ts
│ │ └── Transaction.ts
│ ├── middleware/
│ │ ├── errorHandler.ts
│ │ ├── rateLimiter.ts
│ │ └── requestLogger.ts
│ └── types/index.ts
└── infrastructure/
├── server.ts
└── config/index.ts

Code Highlights

— real excerpts from the backend
shared/kernel/Result.ts
typescript
export class Result<T> {
  private constructor(
    private readonly _isSuccess: boolean,
    private readonly _value?: T,
    private readonly _error?: string
  ) {}

  static ok<T>(value: T): Result<T> {
    return new Result<T>(true, value);
  }

  static fail<T>(error: string): Result<T> {
    return new Result<T>(false, undefined, error);
  }

  map<U>(fn: (val: T) => U): Result<U> {
    if (this._isSuccess) return Result.ok(fn(this._value as T));
    return Result.fail<U>(this._error as string);
  }
}
modules/auth/application/handlers/AuthHandler.ts
typescript
export class AuthHandler {
  constructor(private repo: IAuthRepository) {}

  async register(cmd: RegisterCommand): Promise<Result<TokenPair>> {
    const existing = await this.repo.findByEmail(cmd.email);
    if (existing) return Result.fail("Email already registered");

    const hash = await AuthDomainService.hashPassword(cmd.password);
    const user = await this.repo.create({
      id: uuidv4(), email: cmd.email,
      name: cmd.name, passwordHash: hash,
    });

    const tokens = TokenService.generateTokenPair({
      userId: user.id, email: user.email,
    });

    await EventBus.emit("auth.registered", { userId: user.id });

    return Result.ok(tokens);
  }
}
modules/payments/infrastructure/stripe/StripeService.ts
typescript
import Stripe from "stripe";

export class StripeService {
  static async createPaymentIntent(
    amountCents: number,
    metadata: Record<string, string>
  ): Promise<Stripe.PaymentIntent> {
    return getStripe().paymentIntents.create({
      amount: amountCents,
      currency: "usd",
      metadata,
    });
  }

  static constructWebhookEvent(
    body: Buffer, signature: string
  ): Stripe.Event {
    return getStripe().webhooks.constructEvent(
      body, signature, config.stripe.webhookSecret
    );
  }
}
shared/database/Transaction.ts
typescript
export class Transaction {
  private constructor(private conn: PoolConnection) {}

  static async begin(): Promise<Transaction> {
    const conn = await getConnection();
    await conn.beginTransaction();
    return new Transaction(conn);
  }

  // Usage in OrderRepository:
  // const tx = await Transaction.begin();
  // await tx.execute("INSERT INTO orders ...", [...]);
  // await tx.execute("INSERT INTO order_items ...", [...]);
  // await tx.commit(); // or tx.rollback() on error
}
modules/auth/application/commands/RegisterCommand.ts
typescript
import { z } from "zod/v4";

export const RegisterSchema = z.object({
  email: z.email(),
  password: z.string().min(8),
  name: z.string().min(1).max(100),
});

// Used as Express middleware:
// router.post("/register", validate(RegisterSchema), controller.register);

export function validate(schema: ZodType) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      res.status(400).json({
        status: "error",
        errors: result.error.issues,
      });
      return;
    }
    next();
  };
}
modules/auth/auth.module.ts
typescript
export const AuthModule = {
  name: "auth",
  router: Router(),

  async register(): Promise<void> {
    const repo       = new AuthRepository();
    const handler    = new AuthHandler(repo);
    const controller = new AuthController(handler);

    Container.registerInstance("AuthRepository", repo);
    Container.registerInstance("AuthHandler", handler);

    this.router.use("/", createAuthRoutes(controller));
  },
};

// In server.ts — all 5 modules auto-register:
const modules = [AuthModule, UsersModule, BooksModule,
                  OrdersModule, PaymentsModule];
for (const mod of modules) {
  await mod.register();
  app.use(`/api/${mod.name}`, mod.router);
}
shared/kernel/EventBus.ts
typescript
export class EventBus {
  private static handlers: Map<string, EventHandler[]> = new Map();

  static on<T>(event: string, handler: EventHandler<T>): void {
    const existing = this.handlers.get(event) || [];
    existing.push(handler);
    this.handlers.set(event, existing);
  }

  static async emit<T>(event: string, payload: T): Promise<void> {
    const handlers = this.handlers.get(event) || [];
    await Promise.all(handlers.map(h => h(payload)));
  }
}

// Decoupled inter-module communication:
// AuthHandler:   EventBus.emit('auth.registered', { userId })
// OrderHandler:  EventBus.emit('order.created', { orderId, total })
// PaymentHandler: EventBus.emit('payment.succeeded', { orderId })

Mirrored Architecture

The Flutter app and Node.js backend share the same architectural philosophy. Each layer maps 1:1 across the stack.

Flutter — Clean Architecture

Presentation

BLoC (state management)
Pages & Widgets
go_router navigation

Domain

Entities (Book, Cart, Order)
Use Cases (GetCart, AddToCart)
Repository interfaces

Data

Repository implementations
Hive local datasources
API remote datasources

Config

GetIt dependency injection
Theme & route config
Environment constants
Node.js — Domain-Driven Design

Presentation

Express routes & controllers
Zod validators
Auth middleware (JWT guard)

Application

Command/Query handlers
CQRS orchestration
EventBus integration

Domain

Entities (User, Order, Payment)
Repository interfaces
Domain services (AuthDomainService)

Infrastructure

MariaDB repositories (mysql2)
Stripe SDK integration
bcrypt + JWT crypto
FOUNDING CLIENT PROGRAM

An Investment in Partnership

We're building our portfolio selectively. For our first full-stack showcase projects, we're offering a small number of spots at an introductory rate — in exchange for permission to feature the project as a case study and a detailed testimonial about the experience.

$12,000
$25,000 – $35,000FOUNDING RATE
Full Flutter source code (20+ screens)
Full Node.js backend source code (5 DDD modules)
MariaDB schema + 9 migration files
Real Stripe payment integration
JWT authentication system
API documentation
30 days post-delivery support
Deployment assistance
Limited Availability

Only 3 spots at the founding rate. Once filled, pricing returns to standard market rates.

Delivery Timeline

30–45 days from project kickoff. Includes milestone check-ins and a dedicated communication channel.

The Partnership

In exchange for the founding rate, we ask for a detailed testimonial about your experience and permission to showcase the project in our portfolio.

You Own Everything

Full source code for both platforms. No lock-in, no recurring fees.

Production-Ready

Not a prototype — a deployable, scalable application with real payments.

Enterprise Patterns

Clean Architecture + DDD. The same patterns used by teams at scale.

Everything Included

20+ Flutter Screens

Home, catalog, detail, cart, checkout, orders, profile, auth, and more

Node.js Backend

5 DDD modules: Auth, Users, Books, Orders, Payments

MariaDB Schema

9 tables with foreign keys, indexes, and full migration files

Stripe Integration

Real PaymentIntent flows, webhook handling, secure card processing

JWT Auth System

Login, register, token refresh, password hashing with bcrypt

Full Source Code

Both Flutter and Node.js codebases — yours to own and modify

API Documentation

All endpoints documented with parameters and example responses

30-Day Support

Post-delivery support for bugs, deployment issues, and questions

Deployment Assistance

Help with app store submission, server setup, and DNS configuration

Ready to Build Your
Success Story?

3 founding spots. Full-stack Flutter + Node.js. Real Stripe payments. Your source code. 30–45 day delivery.

No commitment until we align on scope. Free initial consultation.