A lightweight, TypeScript-based backend server for real-time applications, built with Hono, Socket.IO, and Drizzle ORM. Push-Alt supports both SQLite and LibSQL databases, offering flexibility for local development or cloud deployment. Ideal for chat apps, collaborative tools, or any project needing real-time communication.
- Real-time Communication: WebSocket support via Socket.IO for channel-based messaging, triggered via HTTP API.
- Flexible Database: Choose between SQLite (local) or LibSQL (remote) with Drizzle ORM.
- Modular Routing: RESTful API with Hono, organized into controllers (auth, admin, workspace, etc.).
- Security: CORS, admin middleware, and token-based channel access.
- Type Safety: Fully written in TypeScript with strong typing.
- Node.js (v18+ recommended)
- npm (for installing dependencies)
- Clone the repository:
git clone <repository-url> cd push-alt
- Install dependencies:
npm install
- Build the project:
npm run build
Push-Alt uses dotenv
to load environment variables. Create a .env
file in the project root:
DB_TYPE=libsql # 'sqlite' or 'libsql'
DATABASE_URL=http://libsql-server:8080 # LibSQL URL or SQLite file path
DATABASE_AUTH_TOKEN=your-token # Required for LibSQL
CORS_ORIGINS=http://localhost:5173 # Comma-separated list
NODE_ENV=development # Controls migrations (runs in non-production)
SQLITE_DB_PATH=./local.db # Path for SQLite (if DB_TYPE=sqlite)
- Start in development mode (with hot reloading):
npm run dev
- Start the compiled app:
npm run start
+-------------------------------------+
| Client Flow: Token & Messaging |
| (Interacting with Push-Alt Server)|
+-------------------------------------+
|
v
+------------------------------------+
| 1. Request Token |
+------------------------------------+
| POST /api/channel/auth |
| Headers: |
| Content-Type: application/json |
| Body: |
| { |
| "app_id": "your-app-id", |
| "app_key": "your-app-key", |
| "channel_name": "general", |
| "expires_in_seconds": 3600 |
| } |
+------------------------------------+
|
v
+--------------------------------------------------------------+
| 2. Receive Token Response |
+--------------------------------------------------------------+
| Success (200): |
| { |
| "success": true, |
| "token": "random-token", |
| "message_listen_name": "workspace:<id>:general:message", |
| "message": "Channel created", |
| "data": {...} |
| } |
| Failure (401/400): |
| { "success": false, "message": "..." } |
+--------------------------------------------------------------+
|
| (if success)
v
+--------------------------------------------------------------+
| 3. Connect to Socket.IO |
+--------------------------------------------------------------+
| const socket = io('http://localhost:4321', { path: '/ws' }); |
+--------------------------------------------------------------+
|
v
+--------------------------------+
| 4. Join Channel with Token |
+--------------------------------+
| socket.emit('join_channel', { |
| "token": "random-token", |
| "channel_name": "general" |
| }); |
+--------------------------------+
|
+-----------------+-------------------+
| (if joined) | (if error) |
v v |
+----------------+ +----------------+ |
| 5a. Joined | | 5b. Error | |
+----------------+ +----------------+ |
| socket.on('joined', (data) => { | |
| console.log(data.message); | |
| // "Connected to general..." | |
| }); | |
+----------------+ | |
| +-------------------------------+
| | socket.on('error', (err) => {
| | console.error(err.message);
| | });
| +-------------------------------+
|
v
+-----------------------------------------------------------------------+
| 6. Listen for Messages |
+-----------------------------------------------------------------------+
| socket.on("workspace:<id>:general:message", (msg) => { |
| console.log('Received:', msg); // e.g., { text: "Hello, world!" } |
| }); |
| // Use message_listen_name from step 2 |
+-----------------------------------------------------------------------+
|
v
+--------------------------------------------------------+
| 7. (Optional) Send Message |
+--------------------------------------------------------+
| POST /api/message/emit |
| Headers: |
| Content-Type: application/json |
| Body: |
| { |
| "app_id": "your-app-id", |
| "channel_name": "general", |
| "body": { "text": "Hello, world!" } |
| } |
| Response: |
| { "success": true, "message": "Message emitted..." } |
+--------------------------------------------------------+
Generate and apply database migrations with Drizzle:
npm run drizzle:generate
Migrations run automatically on startup unless NODE_ENV=production
.
/api/auth
: Authentication routes/api/admin
: Admin-only routes (protected by middleware)/api/workspaces
: Workspace management/api/channel
: Channel operations (includes/auth
for token generation)/api/message
: Message handling (includes/emit
for broadcasting)/api/setting
: Settings management/api/setup
: Initial setup routes/api/test-connection
: Test database connection (admin-only)
Static files are served from ./webapp/dist
.
Push-Alt uses Socket.IO for WebSocket communication. Connect clients to /ws
:
To join a channel, you need a token. Use the /api/channel/auth
endpoint:
// Example using fetch
fetch('http://localhost:4321/api/channel/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: 'your-app-id',
app_key: 'your-app-key', // Must match the workspace's app_key
channel_name: 'general',
expires_in_seconds: 3600, // Optional, defaults to 1 hour (3600 seconds)
}),
})
.then(res => res.json())
.then(data => {
if (data.success) {
console.log('Token:', data.token);
console.log('Listen to:', data.message_listen_name);
} else {
console.error(data.message);
}
})
.catch(err => console.error(err));
- Request Fields:
app_id
: Unique identifier for the workspace.app_key
: Secret key for the workspace (verified with bcrypt).channel_name
: Name of the channel to create/join.expires_in_seconds
: Optional expiration time in seconds (defaults to 3600 if omitted).
- Response:
token
: Randomly generated token to join the channel.message_listen_name
: Event name for receiving messages (e.g.,workspace:<id>:general:message
).
Use the token from /api/channel/auth
to join the channel:
const socket = io('http://localhost:4321', { path: '/ws' });
socket.emit('join_channel', {
token: 'your-generated-token', // From /api/channel/auth
channel_name: 'general',
});
socket.on('joined', (data) => {
console.log(data.message); // "Connected to general in workspace X"
});
socket.on('error', (err) => {
console.error(err.message);
});
Messages are sent via an HTTP POST request to /api/message/emit
, which broadcasts to connected clients:
// Example using fetch
fetch('http://localhost:4321/api/message/emit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: 'your-app-id',
channel_name: 'general',
body: { text: 'Hello, world!' },
}),
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
// Client-side listener using message_listen_name from /api/channel/auth
let messageListenName; // Set this from the /api/channel/auth response
socket.on(messageListenName, (msg) => {
console.log('Received:', msg); // { text: 'Hello, world!' }
});
Modify the server setup in src/index.ts
. Example:
import createPushAlt from './src/index';
const { server, db } = await createPushAlt({
port: 3000,
corsOrigins: ['https://myapp.com'],
dbType: 'sqlite',
databaseUrl: './myapp.db',
runMigrations: true,
socketOptions: { pingTimeout: 60000 },
});
server(({ app }) => {
// Add custom middleware or routes
app.get('/custom', (c) => c.text('Hello from custom route!'));
});
Option | Type | Default | Description |
---|---|---|---|
port |
number |
4321 |
Server port |
socketOptions |
Partial<ServerOptions> |
{} |
Socket.IO configuration |
corsOrigins |
string[] |
['http://localhost:5173'] |
Allowed CORS origins |
runMigrations |
boolean |
true (non-prod) |
Run DB migrations on startup |
databaseUrl |
string |
From env | Database URL or SQLite path |
databaseAuthToken |
string |
From env | LibSQL auth token |
dbType |
'sqlite' | 'libsql' |
'libsql' |
Database type |
Contributions are welcome! Please:
- Fork the repository.
- Create a feature branch (
git checkout -b feature/my-feature
). - Commit changes (
git commit -m "Add my feature"
). - Push to the branch (
git push origin feature/my-feature
). - Open a pull request.
MIT License (unless specified otherwise in your project).
For issues or questions, open an issue on the GitHub repository or contact the maintainers.