A full‑stack service marketplace web application built with Flask + SQLAlchemy + Bootstrap 5. It supports two roles:
- Users – sign up, post requirements, manage bookings.
- Providers – sign up, list services, browse user posts and bid.
The app includes authentication, a search page with filters and sorting, simple dashboards, and demo pages for bookings/services.
- Auth & Roles: Separate signup/signin for Users and Providers, secure passwords (Werkzeug hashing).
- Search & Sort: Filter services by name, location, category, price range; sort by newest/oldest and price.
- Posts & Bids: Users create posts (requirements). Providers can place bids (model included).
- Dashboards: Role‑aware dashboards routed by session role.
- Demo Pages:
/user/bookings,/provider/servicessample listings. - Responsive UI: Bootstrap 5 + custom CSS (
style.css,index.css,search.css). - SQLite: Auto‑created database under
db/users.db.
project-root/
├── app.py
├── models.py
├── db/ # created on first run
│ └── users.db
├── templates/
│ ├── index.html
│ ├── search.html
│ ├── user_bookings.html
│ └── provider_services.html
└── static/
├── css/
│ ├── style.css
│ ├── index.css
│ └── search.css
└── img/
├── name.png
└── placeholder.png
Important: Flask loads templates from the
templates/folder and static files fromstatic/. Make sure your files match the structure above (move them if needed).
# Python 3.10+ recommended
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS/Linux
source .venv/bin/activatepip install Flask==3.0.3 Flask-SQLAlchemy==3.1.1 Werkzeug==3.0.3Create folders if they don’t exist:
mkdir -p templates static/css static/img dbPlace the HTML files into templates/, the CSS into static/css/, and images (e.g., name.png, placeholder.png) into static/img/.
python app.pyVisit: http://127.0.0.1:5000/
On first run, the SQLite DB will be created at
db/users.db.
app.py uses:
-
SECRET_KEYset inline. For production, supply an environment variable and change app config:app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-only-secret')
-
SQLite path is set to a safe subfolder:
basedir = os.path.abspath(os.path.dirname(__file__)) db_dir = os.path.join(basedir, 'db') os.makedirs(db_dir, exist_ok=True) db_path = os.path.join(db_dir, 'users.db') app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{db_path}"
There are two copies of models across app.py and models.py. Prefer the single‑source approach (see Recommended Cleanup below).
From models.py:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
full_name = db.Column(db.String(100))
email = db.Column(db.String(120), unique=True)
password = db.Column(db.String(200))
phone = db.Column(db.String(20))
address = db.Column(db.String(200))
dob = db.Column(db.String(20))
class Provider(db.Model):
id = db.Column(db.Integer, primary_key=True)
full_name = db.Column(db.String(100))
email = db.Column(db.String(120), unique=True)
password = db.Column(db.String(200))
phone = db.Column(db.String(20))
address = db.Column(db.String(200))
dob = db.Column(db.String(20))
about = db.Column(db.Text)
category = db.Column(db.String(50))
skills = db.Column(db.String(200))
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=False)
budget = db.Column(db.Float, nullable=False)
deadline = db.Column(db.String(20), nullable=False)
user = db.relationship('User', backref='posts')From app.py (extra models used by routes):
class Booking(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
service_name = db.Column(db.String(100))
date = db.Column(db.String(20))
status = db.Column(db.String(20))
user = db.relationship('User', backref='bookings')
class Service(db.Model):
id = db.Column(db.Integer, primary_key=True)
provider_id = db.Column(db.Integer, db.ForeignKey('provider.id'))
title = db.Column(db.String(100))
description = db.Column(db.Text)
price = db.Column(db.Float)
provider = db.relationship('Provider', backref='services')
class Bid(db.Model):
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
provider_id = db.Column(db.Integer, db.ForeignKey('provider.id'), nullable=False)
amount = db.Column(db.Float, nullable=False)
message = db.Column(db.Text)
post = db.relationship('Post', backref='bids')
provider = db.relationship('Provider', backref='bids')Tip: Consolidate all models into
models.pyand import them inapp.pyto avoid schema drift.
GET /→index.html(home + hero + search CTA, modals for auth).GET|POST /search→search.html- POST accepts
name,location,category,priceRange,sortByand returns filtered services. - GET shows latest services (default order: newest).
- POST accepts
POST /signup/user– create a User (prevents email clash with Provider).POST /signup/provider– create a Provider (prevents email clash with User).POST /signin– sign in as User or Provider (shared form; role stored in session).GET /logout– clear session.GET /user/dashboard– shows user profile + their posts.GET /provider/dashboard– shows provider profile + all posts (to bid later).POST /update_profile– update basic profile fields (name/address/dob) by role.POST /create_post– User creates a requirement post.GET /view_bids/<post_id>– view bids for a given post (templatebids.htmlexpected).POST /delete_post/<post_id>– delete user’s own post.GET /user/bookings– demo bookings page.GET /provider/services– demo provider services page.
templates/search.html exposes dropdowns for Location, Category, Price Range, and Sort By. Hidden inputs carry the chosen values to the server. The results section renders cards from Service + Provider info and supports a placeholder image when a provider has no image_url.
Create some quick demo rows via Flask shell:
python
>>> from app import db, User, Provider, Service
>>> db.create_all()
>>> p = Provider(full_name="Jane Waters", email="jane@p.com", password="hashed", phone="+61 412 345 678",
... address="25 King St, Sydney", dob="1990-04-12", about="Experienced cleaner.",
... category="cleaning", skills="deep cleaning,vacuuming,sanitizing")
>>> db.session.add(p); db.session.commit()
>>> s = Service(provider_id=p.id, title="Premium Home Cleaning", description="Deep cleaning and sanitation.", price=149.0)
>>> db.session.add(s); db.session.commit()Replace
"hashed"with an actual hashed password:
from werkzeug.security import generate_password_hash
p.password = generate_password_hash("123")
db.session.commit()-
Single models source: Move all models into
models.pyand import inapp.py:from models import db, User, Provider, Service, Booking, Post, Bid db.init_app(app)
And call
with app.app_context(): db.create_all()at startup. -
Templates: Ensure all templates live in
templates/. Missing pages referenced by routes (e.g.,bids.html,dashboard.html,about_us.html,contact.html,blog.html) should be added to avoidTemplateNotFound. -
Static paths: In templates, reference CSS/JS via
url_for('static', filename='css/style.css')to avoid broken relative paths. -
Forms JS: Ensure
static/fuc/account.jsandstatic/fuc/search.jsexist or remove those<script>tags. -
Provider images: If you use
service.provider.image_url, add that column toProvideror guard with default placeholders. -
CSRF & Security: Consider Flask‑WTF for CSRF protection; move secrets to environment variables.
-
Migrations: Add Alembic/Flask‑Migrate for schema changes.
TemplateNotFound: Check file names/locations and that you’re callingrender_template('name.html')for a file undertemplates/.- Static files not loading: Use
{{ url_for('static', filename='css/style.css') }}and ensurestatic/exists. BadRequestKeyError: Access missing form keys safely, e.g.request.form.get('field').- Duplicate emails: Both signup routes prevent email reuse across roles.
- Flask session issues: After logout, both
emailandrolekeys are removed (session.modified = True).
- Provider profiles (image, portfolio, availability).
- Real messaging between Users and Providers.
- Invoices & payments (intent → hold → capture).
- Reviews & ratings after completion.
- Notifications (success toasts + email).
- Pagination for search results.
- RBAC hardening and CSRF everywhere.
Add your preferred license (MIT, Apache-2.0, etc.).
Built with ❤️ using Flask, SQLAlchemy, and Bootstrap 5.