Skip to content

Creating New Streamlit Apps

A guide based on our existing app patterns and best practices

Project Structure

Based on our existing apps, use this standard structure:

your-app/
├── Streamlit/
│   ├── app_name.py              # Main entry point
│   ├── pages/                   # Multi-page app structure
│   │   ├── 1_📤_Upload.py      # Numbered pages with emoji icons
│   │   ├── 2_📊_Analysis.py
│   │   └── 3_⚙️_Settings.py
│   ├── utils/                   # Shared utilities
│   │   ├── __init__.py
│   │   ├── common.py           # Common functions and constants
│   │   └── storage.py          # Data handling utilities
│   ├── requirements.txt
│   ├── railway.toml           # Railway deployment config
│   └── Dockerfile            # Container configuration
├── data/                      # Local data directory
│   ├── raw/
│   ├── processed/
│   └── parquet/
└── README.md

Step 1: Main App Entry Point

Create your main app file (your_app.py):

"""
Your App Name - Brief Description

Main entry point for the multipage Streamlit application.
Production-ready system for [your specific use case].

🏠 Home Page - Overview and getting started
"""

import streamlit as st
import sys
from pathlib import Path

# Add utils to path
sys.path.append(str(Path(__file__).parent))
from utils.common import get_dark_theme_css, DATA_DIR

# Page configuration
st.set_page_config(
    page_title="Your App - Home",
    page_icon="🏠",
    layout="wide",
    initial_sidebar_state="expanded"
)

# Apply consistent styling
st.markdown(get_dark_theme_css(), unsafe_allow_html=True)

def main():
    """Main home page content"""

    # Main header
    st.markdown("""
    <div class="main-header">
        <h1>🏠 Your App Name</h1>
        <h3>Brief Description</h3>
        <p>Detailed description of what your app does</p>
    </div>
    """, unsafe_allow_html=True)

    # Sidebar with quick stats and navigation
    with st.sidebar:
        st.title("📊 Quick Stats")
        # Add relevant metrics here
        st.metric("📁 Total Items", "0")

        st.markdown("---")
        st.markdown("**🚀 Quick Start**")
        st.info("👆 Use the page navigation above to get started")

    # Main content layout
    col1, col2 = st.columns([2, 1])

    with col1:
        st.subheader("🎯 What does this app do?")
        st.markdown("""
        Brief explanation of your app's purpose and main features.

        **Key Capabilities:**
        - 📤 **Feature 1**: Description
        - 📊 **Feature 2**: Description  
        - ⚙️ **Feature 3**: Description
        """)

        # Feature cards
        st.subheader("✨ Key Features")
        features = [
            {
                "title": "📤 Feature 1",
                "description": "Detailed description of first feature",
                "page": "1_📤_Feature1"
            },
            {
                "title": "📊 Feature 2", 
                "description": "Detailed description of second feature",
                "page": "2_📊_Feature2"
            }
        ]

        for feature in features:
            st.markdown(f"""
            <div class="feature-card">
                <h4>{feature['title']}</h4>
                <p>{feature['description']}</p>
            </div>
            """, unsafe_allow_html=True)

    with col2:
        st.subheader("📋 How to Use")
        st.markdown("""
        ### 1. Step One
        Description of first step

        ### 2. Step Two  
        Description of second step

        ### 3. Step Three
        Description of third step
        """)

if __name__ == "__main__":
    main()

Step 2: Create Utils Module

Create utils/common.py for shared functionality:

"""
Common Utilities

Shared utilities and constants for your Streamlit app.
"""

import streamlit as st
import pandas as pd
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
import json

# App constants - centralized for consistency
DATA_DIR = Path(__file__).parent.parent.parent / "data"
RAW_DIR = DATA_DIR / "raw"
PROCESSED_DIR = DATA_DIR / "processed"
PARQUET_DIR = DATA_DIR / "parquet"

# Ensure directories exist
for directory in [RAW_DIR, PROCESSED_DIR, PARQUET_DIR]:
    directory.mkdir(parents=True, exist_ok=True)

def get_dark_theme_css() -> str:
    """Common dark theme CSS used across all pages"""
    return """
    <style>
        /* Dark theme detection and adaptive styling */
        .main-header {
            background: linear-gradient(90deg, #1f4e79, #2c5f8a);
            color: white;
            padding: 1.5rem;
            border-radius: 10px;
            margin-bottom: 2rem;
            text-align: center;
        }

        .feature-card {
            background-color: var(--background-color-secondary, #f8f9fa);
            padding: 1.5rem;
            border-radius: 10px;
            border-left: 4px solid #1f4e79;
            margin-bottom: 1rem;
            color: var(--text-color, inherit);
            border: 1px solid var(--border-color, rgba(49, 51, 63, 0.2));
        }

        /* Dark theme overrides */
        @media (prefers-color-scheme: dark) {
            .feature-card {
                background-color: rgba(49, 51, 63, 0.3) !important;
                color: white !important;
                border: 1px solid rgba(250, 250, 250, 0.2) !important;
            }
        }

        /* Streamlit dark theme class detection */
        [data-theme="dark"] .feature-card,
        .stApp[data-theme="dark"] .feature-card {
            background-color: rgba(49, 51, 63, 0.3) !important;
            color: white !important;
            border: 1px solid rgba(250, 250, 250, 0.2) !important;
        }

        .metric-highlight {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 1rem;
            border-radius: 10px;
            text-align: center;
            margin: 1rem 0;
        }

        .status-success {
            background-color: rgba(40, 167, 69, 0.1);
            border: 1px solid #28a745;
            color: #28a745;
            padding: 0.5rem;
            border-radius: 5px;
        }

        .status-warning {
            background-color: rgba(255, 193, 7, 0.1);
            border: 1px solid #ffc107;
            color: #856404;
            padding: 0.5rem;
            border-radius: 5px;
        }

        .status-error {
            background-color: rgba(220, 53, 69, 0.1);
            border: 1px solid #dc3545;
            color: #dc3545;
            padding: 0.5rem;
            border-radius: 5px;
        }
    </style>
    """

def show_page_header(title: str, description: str, icon: str = "🏠"):
    """Standard page header used across all pages"""
    st.markdown(f"""
    <div class="main-header">
        <h1>{icon} {title}</h1>
        <p>{description}</p>
    </div>
    """, unsafe_allow_html=True)

def show_status_card(title: str, status: str, description: str = ""):
    """Reusable status card component"""
    status_class = f"status-{status.lower()}"
    st.markdown(f"""
    <div class="{status_class}">
        <h4>{title}</h4>
        {f"<p>{description}</p>" if description else ""}
    </div>
    """, unsafe_allow_html=True)

@st.cache_data(ttl=300)
def load_data_file(file_path: Path) -> pd.DataFrame:
    """Cache data loading with 5-minute TTL"""
    try:
        if file_path.suffix == '.parquet':
            return pd.read_parquet(file_path)
        elif file_path.suffix == '.csv':
            return pd.read_csv(file_path)
        else:
            st.error(f"Unsupported file type: {file_path.suffix}")
            return pd.DataFrame()
    except Exception as e:
        st.error(f"Error loading {file_path.name}: {e}")
        return pd.DataFrame()

def save_data_with_timestamp(df: pd.DataFrame, base_name: str, format: str = 'parquet') -> Path:
    """Save data with timestamp in filename"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    if format == 'parquet':
        file_path = PARQUET_DIR / f"{base_name}_{timestamp}.parquet"
        df.to_parquet(file_path)
    else:
        file_path = PROCESSED_DIR / f"{base_name}_{timestamp}.csv"
        df.to_csv(file_path, index=False)

    return file_path

def format_number(value: float, format_type: str = "currency") -> str:
    """Format numbers consistently across the app"""
    if pd.isna(value):
        return "N/A"

    if format_type == "currency":
        return f"${value:,.2f}"
    elif format_type == "percentage":
        return f"{value:.2%}"
    else:
        return f"{value:,.2f}"

Step 3: Create Page Templates

Create pages using this template (pages/1_📤_Example_Page.py):

"""
Example Page - Description of page functionality
"""

import streamlit as st
import pandas as pd
from pathlib import Path
import sys

# Add utils to path
sys.path.append(str(Path(__file__).parent.parent))
from utils.common import get_dark_theme_css, show_page_header, DATA_DIR

# Page configuration
st.set_page_config(
    page_title="Your App - Example Page",
    page_icon="📤",
    layout="wide"
)

# Apply styling
st.markdown(get_dark_theme_css(), unsafe_allow_html=True)

def main():
    """Main page content"""
    show_page_header("Example Page", "Description of what this page does", "📤")

    # Sidebar
    with st.sidebar:
        st.title("⚙️ Settings")
        # Add page-specific settings here

        st.markdown("---")
        st.info("💡 **Tip**: Page-specific help text here")

    # Main content
    st.subheader("🎯 Main Functionality")

    # Example form
    with st.form("example_form"):
        col1, col2 = st.columns(2)

        with col1:
            input1 = st.text_input("Input 1", help="Description of input")

        with col2:
            input2 = st.selectbox("Input 2", ["Option A", "Option B"])

        submitted = st.form_submit_button("Process", use_container_width=True)

        if submitted:
            if input1 and input2:
                # Process the inputs
                st.success("✅ Processing complete!")

                # Display results
                with st.expander("📋 Results", expanded=True):
                    st.write(f"Input 1: {input1}")
                    st.write(f"Input 2: {input2}")
            else:
                st.error("❌ Please fill in all required fields")

if __name__ == "__main__":
    main()

Step 4: Essential Files

requirements.txt

streamlit>=1.28.0
pandas>=2.0.0
pyarrow>=13.0.0
supabase>=2.0.0
python-dotenv>=1.0.0

railway.toml

[build]
builder = "NIXPACKS"

[deploy]
startCommand = "streamlit run your_app.py --server.port $PORT --server.address 0.0.0.0"

[[services]]
name = "your-app"

Dockerfile (optional)

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8501

CMD ["streamlit", "run", "your_app.py", "--server.port=8501", "--server.address=0.0.0.0"]

Step 5: Best Practices from Our Apps

1. Consistent Navigation

  • Use numbered pages with emoji icons: 1_📤_Upload.py
  • Keep page titles consistent: "Your App - Page Name"
  • Use descriptive page icons that match functionality

2. Sidebar Structure

Always include in sidebar: - App status/quick stats - Page-specific settings - Help/tip sections - Navigation aids

3. Layout Patterns

# Standard two-column layout
col1, col2 = st.columns([2, 1])

with col1:
    # Main content
    pass

with col2:
    # Secondary content, help, or settings
    pass

4. Error Handling

try:
    # Operation that might fail
    result = risky_operation()
    st.success("✅ Operation successful!")
except Exception as e:
    st.error(f"❌ Error: {e}")
    st.exception(e)  # For debugging

5. Data Caching

@st.cache_data(ttl=300)  # Cache for 5 minutes
def load_expensive_data():
    # Expensive operation
    return data

# For database connections
@st.cache_resource
def get_database_connection():
    return create_connection()

6. Session State Management

# Initialize session state
if 'key' not in st.session_state:
    st.session_state.key = default_value

# Use session state
st.session_state.key = new_value

7. Progress Indicators

# For long operations
progress_bar = st.progress(0)
for i, item in enumerate(items):
    # Process item
    progress_bar.progress((i + 1) / len(items))

progress_bar.empty()  # Clean up

Step 6: Testing and Deployment

Local Development

cd your-app/Streamlit
pip install -r requirements.txt
streamlit run your_app.py

Railway Deployment

  1. Connect your repository to Railway
  2. Ensure railway.toml is configured
  3. Set environment variables in Railway dashboard
  4. Deploy with: railway up

Additional Best Practices

Quick Wins (Copy These Patterns)

Loading States - Always show users something is happening:

with st.spinner("Processing..."):
    result = your_function()

Environment Check - Catch missing config early:

if not os.getenv("SUPABASE_URL"):
    st.error("Missing SUPABASE_URL environment variable")
    st.stop()

Input Validation - Check data before processing:

if uploaded_file is not None:
    if not uploaded_file.name.endswith('.csv'):
        st.error("Please upload a CSV file")
    else:
        # Process file

Disable Buttons During Processing - Prevent double submissions:

processing = st.session_state.get('processing', False)
if st.button("Submit", disabled=processing):
    st.session_state.processing = True
    # Do work...
    st.session_state.processing = False

Common Patterns Summary

File Organization: Main app + pages/ + utils/ structure Styling: Dark theme compatible with consistent CSS
Navigation: Numbered emoji pages with sidebar navigation Data Handling: Cached loading with timestamp-based saving Error Handling: Try/catch with user-friendly messages Forms: Use st.form() for better UX Layout: Two-column layouts with main + sidebar content Loading States: Always show progress to users

This structure ensures consistency across all our Streamlit applications while remaining flexible for different use cases.


This guide is based on our production Streamlit apps: Warehouse, Streamlit-Testing, and Sigma-Surf-Strategy patterns.