Skip to main content

Deploy full-featured web apps in minutes. Auth, payments, admin, email, database — one import.

Project description

Tidepool

    ┌─────────────────────────────────────────────┐
    │  build locally  →  deploy in 30s  →  live   │
    │       ↑                                ↓    │
    │    iterate        auth · payments · email    │
    │       ↑              db · files · http       │
    │       └──────────  <you>.tidepool.sh  ──┘   │
    └─────────────────────────────────────────────┘

Build and deploy full-featured web apps in minutes. Auth, payments, admin, email, file storage, database, background tasks — all from a single tp object.

Designed for AI agents and humans with CLI coding tools like Claude Code. The API is self-documenting (curl https://tidepool.sh/api), and every feature is available first and foremost via the command line.

Tools such as auth flows, Stripe payments, admin and database ORM, multi-page routing, file storage, email, a key-value database, background tasks, etc are all built and documented with AI agents in mind. These tools are available via a single tp object; they are meant to provide abstractions for infrastructure that is hard for an AI to set up (eg, third party subscriptions that take lots of clicks/config). We abstract these away so that the AI can move fast in terms of core site design, logic, and styling. Logging and API errors/warnings/tips are meant to be as detailed as possible so that an agent, on its own, can quickly understand and solve whatever problems come up.

Pods scale horizontally to 10+ replicas with shared Postgres/Redis/R2 — enough for a Substack clone at 20k–50k DAU or a Reddit-style site at 5k–15k DAU. The goal: a fairly advanced website up and running in an afternoon. No servers, no Docker, no AWS.

Quickstart

Install

pip install tidepool

Init a pod

tidepool init my-blog
cd my-blog

Creates a directory with a default main.py:

import tp
tp.page('/', '<h1>Hello from Tidepool!</h1>')

Develop locally

tidepool dev
# Pod runs at http://localhost:8000

The dev server replicates production pod behavior: file I/O goes to ./tp_data/files/, tp.db persists to a JSON file, tp.state is readable at ?format=json. Stripe, R2, and email are optional — the app runs without them.

Deploy

tidepool --url https://tidepool.sh register --email you@example.com
# verify email, then:
tidepool deploy
# Pod is live at https://my-blog.tidepool.sh in ~30 seconds

deploy auto-discovers source files and uploads tp_data/files/ (images, media) and tp_data/secrets.json automatically.

Push & Pull

Pull a live pod to develop locally — all state comes with it:

tidepool pull abc123
# Creates my-blog/ with source files + tp_data/ (db, secrets, files)
cd my-blog
tidepool dev
# Edit code, add data, test locally...
tidepool push              # pushes everything back (hash remembered from pull)

pull downloads source files, tp.dbtp_data/db.json, tp.secretstp_data/secrets.json, and pod files → tp_data/files/. The dev server reads all of these natively — no conversion needed.

push auto-discovers source files (same as deploy) and uploads them along with tp_data/db.json (merge by default), tp_data/secrets.json, and all files in tp_data/files/. Pushing source files triggers a pod restart. Use -y to skip the confirmation prompt.

tidepool pull abc123 --dir .   # pull into current directory instead of a subdirectory
tidepool push abc123           # explicit hash (overrides remembered hash)
tidepool push --file main.py   # push specific files instead of auto-discover
tidepool push --secret STRIPE_KEY=sk_xxx               # override a secret
tidepool push --replace-db     # replace all db keys instead of merging
tidepool push --sync           # delete remote files not present locally
tidepool push -y               # skip confirmation prompt

.tpignore

Create a .tpignore file to exclude files from deploy and push (same syntax as .gitignore):

# Directories (trailing slash)
data/
notebooks/

# File patterns
*.csv
*.npy
*.pkl
*.parquet
pipeline.py
build_log.*

Always ignored regardless of .tpignore: tp_data/, __pycache__/, .git/, venv/, .venv/, node_modules/, dotfiles, and build_log.*. Files over 50MB are skipped automatically with a warning.

tidepool init generates a starter .tpignore. For existing projects, create one before your first deploy or push to avoid uploading data files, models, or build artifacts.

Eject Mode

For full control over the runtime, eject the internals into your project:

tidepool eject
# Copies tp_runtime.py, tp_server.py, tp_backend.py, tp_templates/ into your project

These files are now yours to modify. tidepool dev, deploy, and push auto-detect eject mode when tp_server.py exists in the project directory — no flags needed. To undo, delete the ejected files.

Runtime Tools

Use import tp at the top of every .py file. main.py runs once at startup to configure the pod — set auth, payments, seed data, register routes. The server dispatches requests directly to handlers.

Name Description Usage
tp.route Register a request handler with path params @tp.route('/post/:slug', methods=['GET'])
tp.page Register a static HTML page (no handler) tp.page('/about', '<h1>About</h1>')
tp.auth Full auth system. Presets: 'paywall' (pay-first + magic link) or 'standard' (email/password) tp.auth = 'paywall' or tp.auth = 'standard'
tp.payments Stripe subscriptions and one-time purchases (in cents) tp.payments = {products: [{id: 'pro', price: 500, recurring: 'month'}]}
tp.admin Auto-generated admin panel at /_admin/ tp.admin = {users: ['admin@example.com']}
tp.create_user Create user with hashed password (idempotent) tp.create_user('sam@x.com', 'pass', subscriptions={'pro': True})
tp.db Key-value store, 1GB limit, persisted across runs tp.db.set('post:slug', {...}) / tp.db.get('post:slug')
tp.files File storage (R2 in prod, 50GB), served at /_files/ tp.files.write('photo.jpg', data) / tp.files.read('photo.jpg')
tp.email Send email with optional HTML and attachments tp.email('user@x.com', 'Subject', 'body', html='<p>hi</p>')
tp.http HTTP client (same API as requests), 200 req/60s, SSRF-protected tp.http.post(url, json=payload, headers={...})
tp.markdown Convert markdown to HTML (tables, code, footnotes) html = tp.markdown('# Hello\n\nWorld')
tp.secrets Read-only dict of deploy-time credentials api_key = tp.secrets['STRIPE_KEY']
tp.state Public app state dict, readable at ?format=json tp.state = {'status': 'live'}
tp.publish Update public JSON state (ETag-supported polling) tp.publish({'messages': msgs})
tp.background Background tasks (max 5). seconds<=0: once, >0: loop @tp.background(seconds=3600)

Handler return values: str → 200 HTML, dict/list → 200 JSON, int → status code, tuple(body, status) → body + status, None → 303 redirect, generator → SSE stream.

Request object: Handler receives (req, **params). Attributes: req.path, req.method, req.query, req.user (dict or None), req.body (dict), req.files (dict of upload metadata — file data auto-saved to tp.files).

Auth details

Auth presets: tp.auth = 'paywall' for payment-first apps (no signup form, accounts auto-created at checkout, magic link for return logins — pair with tp.payments). tp.auth = 'standard' for traditional email/password signup with confirmation, reset, and magic link. Customize after setting a preset: tp.auth['required'] = ['/dash/*'], tp.auth['theme'] = {'accent': '#e74c3c'}. Or pass a full dict for manual control: tp.auth = {required: ['/dash/*'], signup: True, reset: True, oauth: ['google']}.

Email confirmation on by default — signup_confirm: False to disable. req.user in handlers gives the logged-in user including subscriptions, purchases, and avatar_url (from Google profile). Theme: theme: {page: '<html>...{content}...</html>'} wraps auth pages in your layout; {content} receives the form, {title} the page title. Simpler: theme: {accent: '#color', css: '...'}.

Google OAuth setup: Add google_client_id and google_client_secret to tp_data/secrets.json. Get credentials at Google Cloud Console → Create OAuth 2.0 Client ID (Web application). Add authorized redirect URI: http://localhost:8000/_auth/oauth/google/callback for dev, https://yourdomain.com/_auth/oauth/google/callback for prod. That's it — the server handles the rest.

Payments details

Set tp.payments = {products: [{id: 'pro', name: 'Pro', price: 500, recurring: 'month'}]}. Users pay at /_pay/pro, manage subscriptions at /_pay/portal. recurring: 'month'/'year' for subscriptions; omit for one-time. Dev mode simulates purchases instantly. Prod requires tidepool stripe-connect (one-time setup).

Admin details

Set tp.admin = {models: {post: {fields: {title: 'string', body: 'text', tier: 'choice:free,pro'}, display: ['title']}}}. Field types: string, text, bool, number, date, choice:a,b,c. Plus read-only views of users, payments, emails, files. If not set, auto-inferred from tp.db key patterns. Admin access control: with tp.auth configured, set users: ['admin@example.com'] to restrict to specific emails (otherwise any logged-in user can access). Without tp.auth, admin is open in dev and key-gated in prod (key printed in server logs at startup, access via /_admin?key=<key>).

Background tasks

@tp.background()  # runs once at startup
def migrate(tp):
    if not tp.db.get('_migrated_v2'):
        for key, val in tp.db.prefix('post:'):
            val['version'] = 2
            tp.db.set(key, val)
        tp.db.set('_migrated_v2', True)

@tp.background(seconds=3600)  # every hour
def send_digest(tp):
    for email, user in tp.users().items():
        if user.get('subscriptions', {}).get('digest'):
            posts = tp.db.prefix('post:', reverse=True, limit=5)
            tp.email(email, 'Hourly Digest', '\n'.join(t for _, t in posts))

Server-Sent Events (SSE)

Return a generator from any route handler to stream real-time events:

@tp.route('/feed/live')
def live_feed(req):
    def stream():
        last_count = 0
        while True:
            messages = tp.db.prefix('msg:', reverse=True, limit=20)
            if len(messages) != last_count:
                last_count = len(messages)
                yield {'messages': [m for _, m in messages]}
            time.sleep(2)
    return stream()

Client-side: new EventSource('/feed/live'). Max 100 concurrent SSE connections per pod.

Static files & templating

  • Static files — Files in static/ alongside main.py are served at /static/<path>.
  • Jinja2 — Pre-installed. from jinja2 import Environment, FileSystemLoader; env = Environment(loader=FileSystemLoader('templates'), autoescape=True). Render: env.get_template('page.html').render(posts=p).

Full API reference

curl https://tidepool.sh/api

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tidepool-0.1.2.tar.gz (53.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tidepool-0.1.2-py3-none-any.whl (51.8 kB view details)

Uploaded Python 3

File details

Details for the file tidepool-0.1.2.tar.gz.

File metadata

  • Download URL: tidepool-0.1.2.tar.gz
  • Upload date:
  • Size: 53.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tidepool-0.1.2.tar.gz
Algorithm Hash digest
SHA256 fcd959c1be083fe935b8984dd2e24063ac1d9c56f6b85992c784a310a47dc095
MD5 be849f3023c1c9e9c7a2ddce6dfcd886
BLAKE2b-256 bc97a943cb6812b83a754699930b3d559347e535c8de6b7daa506ba4d8579518

See more details on using hashes here.

File details

Details for the file tidepool-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: tidepool-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 51.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tidepool-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a87ce949163143b61610779702a7c31c184eaf3ac6efa1f3a69c5439eb46efb2
MD5 745ad710774f29e2057e66b4c91239c6
BLAKE2b-256 f2f5329b2db0ab81ce6bb3eb2b0eda6f80ba7fe3cc0d7601c1b1e5d0032a9beb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page